Hacking Protected Java-Based Programs
Our article is about vulnerabilities in Java application development. It is intended for developers of Java and obfuscators.
Join the DZone community and get the full member experience.
Join For FreeThis article provides examples of hacking techniques that can help Java developers avoid vulnerabilities in their programs. It is not intended to train hackers but rather for naive developers who think that standard obfuscators will save them from their intellectual property theft.
There will be logical gaps in the text, but only for brevity. Any experienced developer or hacker can easily complete them.
All code examples are taken from real applications.
Case 1: Authentication Server
The Client-Server model appeared long before the Internet and is still used everywhere. At the same time, a myth arose that it is impossible to build software protection without the Client-Server part. Below, however, we will show that this is just a myth.
The diagram showing the Server-Client data flows is as follows. Fragments of the program are outside for clarity. The so-called critical points are circled. What this is will become clear from what follows.
The myth about the reliability of the authentication server is quite common, so we'll start with it. We have a server, which receives a request from a program and, after checking, sends a response: will the program continue to work, or is paid registration required? Let's find the point where we get a request simply by searching for classes for working with a LAN or the Internet, i.e., classes from package java.net.*
, e.g., HttpURLConnection
, and similar.
Let us look at this diagram from our point of view, i.e., developer, and then from a hacker's point of view. Two important points are in the application used in this model: the point where a request is sent and the second one is a point received a response from the server.
The snippet of the original Java method for performing request-response actions:
boolean authenticate(String requestURL, String params) {
URL url = null;
HttpURLConnection conn = null;
try {
url = new URL(requestURL);
conn = (HttpURLConnection) url.openConnection();
conn.connect();
} catch (IOException e) {
showError("Failed connect to " + requestURL + ".");
return false; // Warning
}
String response = "";
if (conn != null) {
try (OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream())) {
writer.write(params); // Warning
writer.flush(); // Warning
writer.close();
} catch (IOException e) {
showError("Failed write params " + params + ".");
return false; // Warning
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
conn.getInputStream()))) {
String line = "";
while ((line = reader.readLine()) != null) {
response += line + "\n";
}
reader.close();
} catch (IOException e) {
showError("Failed read " + response + ".");
return false; // Warning
}
if (conn != null)
conn.disconnect();
}
return response.indexOf("activated") > -1; // Error
}
Vulnerable commands are followed by comments.
The same Java method after obfuscation as a hacker sees it:
static boolean Z(String a, String aa) {
String var2 = a;
HttpURLConnection var4 = null;
try {
(var4 = (HttpURLConnection)(new URL(var2)).openConnection()).connect();
} catch (IOException var36) {
return true; /* false; */
}
String var6 = "";
if (var4 != null) {
Object var5;
Throwable var10000;
try {
a = null;
var5 = null;
try {
OutputStreamWriter var3 = new OutputStreamWriter(var4.getOutputStream());
try {
// var3.write(a);
// var3.flush();
var3.close();
} finally {
if (var3 != null) {
var3.close();
}
}
} catch (Throwable var38) {
if (a == null) {
var10000 = var38;
} else {
if (a != var38) {
a.addSuppressed(var38);
}
var10000 = a;
}
// throw var10000;
}
} catch (IOException var39) {
z("Failed connect to " + a + ".");
return true; /* false; */
}
HttpURLConnection var45;
label534: {
try {
a = null;
var5 = null;
try {
BufferedReader var43 = new BufferedReader(new InputStreamReader(
var4.getInputStream()));
boolean var21 = false;
try {
var21 = true;
a = "";
BufferedReader var44 = var43;
while(true) {
if ((a = var44.readLine()) == null) {
var43.close();
var21 = false;
break;
}
var6 = var6 + a + "\n";
var44 = var43;
}
} finally {
if (var21) {
if (var43 != null) {
var43.close();
}
}
}
if (var43 != null) {
var45 = var4;
var43.close();
break label534;
}
} catch (Throwable var41) {
if (a == null) {
var10000 = var41;
} else {
if (a != var41) {
a.addSuppressed(var41);
}
var10000 = a;
}
// throw var10000;
}
} catch (IOException var42) {
z("Failed to read " + var2 + ".");
return true; /* false; */
}
var45 = var4;
}
if (var45 != null) {
var4.disconnect();
}
}
return true; /* var6.indexOf(z("0'%-'%%!5")) > -1; */
}
It's easy to see that by replacing line 97, as well as all previous return...
s with return true
, and comment lines 19 and 20 we get a free version of our program. Note that the server's attempts to determine who is currently applying for authentication and how many such requests will be unsuccessful (this is the case of a license for several persons).
In obfuscated code, finding the output of a method can be much more difficult, but with the pathfinder method, or better like dogs find prey, it can be found sooner or later. The hacker starts from the first definition of the HttpURLConnection
or similar class variable and follows the trail leading to the critical point: var4 -> var4 -> ... var3 -> var3 ... -> var4 -> return ...
the rest of the garbage introduced by the obfuscator can be ignored. In this case, as in many others, the security of your program depends only on the ingenuity of the hacker and not on your efforts. We'll talk about this in the next part.
Case 2: Pay and Play
Below is the Pay and Play diagram showing corresponding components and data flows.
This model has no trial. First, you pay, and then you get it. A hacker has nothing to hack.
The following sentences have become a common rule for developers: “With enough time and effort, almost any code can be reverse-engineered. Obfuscators can make reverse engineering more difficult and economically impractical.” But this is a mistaken opinion. There is a so-called “Stole Once and Sold a Lot” attack. A hacker hacks a program not for himself but to sell it. For example, you can still buy illegal copies of MS Office, Windows 7 or 10, and many other software online for next to nothing.
Thus, the hacker needs to buy the program and perform the steps described earlier for the Client-Server scheme: replace the corresponding line accepting the Activation Key with return true
and delete the command in the payment order.
To repel this simplest attack, the developer encrypts part of the code using any Java cryptographic algorithm with a key received from the server (Activation Key). The encrypted part includes key authentication, and launching the main program if successful. Below are the code snippets, including the code to decrypt and load the class.
This code is used here as well as in the next part, Authenticator
.
public class Authenticator {
public Authenticator(byte[] key, long ... l) {
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec iv = new IvParameterSpec(key);
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
CipherInputStream cis = new CipherInputStream(getClass()
.getResourceAsStream("anyfolder/anyname"), cipher);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
copyStream(cis, baos);
byte[] bytes = baos.toByteArray();
Class<?> klass = new ClassLoader() {
Class<?> define(byte[] bytes, int offset, int length) {
Class<?> klass = super.defineClass(null, bytes, 0, length);
return klass;
}
}.define(bytes, 0, bytes.length);
klass.getConstructor().newInstance();
} catch (Throwable t) {
System.exit(1);
}
}
}
Also, Launcher
, which should be encrypted with the same Activation Key and placed into any resource folder.
public class Launcher {
args = Preloader.args;
public Launcher() {
Application.run(args);
}
}
Where enclosing Preloader
class which sets args
values and the Activation Key received from the Server after payment confirmation.
public class Preloader {
private static byte[] key;
public static String[] args;
public static void main(String[] args) {
receiveConfirmation();
encryptClass();
new Authenticator(key);
}
private static void receiveConfirmation() {
String confirm = responce();
String[] parts = confirm.split(":");
key = hexToBytes(parts[0]);
}
private static void encryptClass() {
IvParameterSpec iv = new IvParameterSpec(key);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
InputStream is = Preloader.class.getResourceAsStream("/Launcher.class");
CipherInputStream cis = new CipherInputStream(is, cipher);
String file = "C:\\Workspaces\\anyfolder\\anyname";
File targetFile = new File(file);
OutputStream outStream = new FileOutputStream(targetFile);
copyStream(cis, outStream);
cis.close();
outStream.close();
} catch (Throwable t) {
System.exit(1);
}
}
private static byte[] hexToBytes(String hex) {
byte[] bytes = new byte[hex.length() / 2];
for (int i = 0; i < bytes.length; i++) {
try {
bytes[i] = (byte) (0xff & Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16));
} catch (Exception e) {
return null;
}
}
return bytes;
}
private static void copyStream(InputStream in, OutputStream out) {
byte[] buffer = new byte[4096];
int read;
try {
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
} catch (IOException e) {
System.exit(1);
}
}
private static void sendPaymentOrder(float sum) {
...
}
private static String responce() {
...
}
}
The advantages of this method of protection are obvious:
- An absence of logical expressions and variables that could be modified
- The pathfinder trace ends at the Authenticator class, which cannot be spoofed or bypassed
- There is no entry point to the main program, nor even its name
We leave aside the issues of copy protection.
Renaming variables, files, and classes do not affect the security level but may create an impression.
Case 3: Time Bomb
This diagram shows the parts and data flows of the Time Bomb model.
This model is for time-limited trials and subscriptions.
Here the counterclockwise timer acts as a server. It is located inside the program since placing it on the server will return us to the previous scheme. The request is "How much time is left" and the response is time > 0 ? run : exit
. The critical point here is the counter itself and the output of the counter, similar to this: request->call counter->response->switch expired/no->.
Static fields start and period are added to Launcher
class and the constructor is a little bit modified.
public class Launcher {
private static long start;
private static long period;
private static String[] args;
public class Application {
public static void main(String[] args) {
...
}
}
static {
args = Preloader.args;
start = Preloader.start;
period = Preloader.period;
}
public Launcher() {
if (System.currentTimeMillis() - start < period) {
Application.main(args);
} else {
System.exit(1);
}
}
}
Unlike Case 1, a hacker cannot find the Launcher class using Java keywords (methods) related to the time and the line or lines to be changed.
Therefore, this class must be reliably hidden from a hacker. Today encryption is the most suitable means for this. We do the following. Firstly, we encrypt Launcher.class
bytes, secondly, we move them into a folder /anyfolder
, rename the class to anyname
, and then decrypt it using the same key with the Authenticator
class, as in Case 2.
public class Authenticator {
public Authenticator(byte[] key, long ... l) {
...
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
...
klass.getConstructor().newInstance();
} catch (Throwable t) {
System.exit(1);
}
}
}
Preloader
and Launcher
classes are modifications of the Pay and Play model classes; parameters have been added for the Start and Time of work, start
and period
, respectively, as well as sum.
public class Preloader {
private static float sum = 1000000.00f; // added for Case 3
private static byte[] key;
public static String[] args;
public static long start; // added for Case 3
public static long period; // added for Case 3
public static void main(String[] args) {
sendPaymentOrder(sum); // added for Case 3
receiveConfirmation();
encryptClass();
new Authenticator(key, start, period);
}
private static void receiveConfirmation() {
String confirm = responce();
String[] parts = confirm.split(":");
key = hexToBytes(parts[0]);
start = Long.parseLong(parts[1]); // added for Case 3
period = Long.parseLong(parts[2]); // added for Case 3
}
private static void encryptClass() {
IvParameterSpec iv = new IvParameterSpec(key);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
InputStream is = Preloader.class.getResourceAsStream("/Launcher.class");
CipherInputStream cis = new CipherInputStream(is, cipher);
String file = "C:\\Workspaces\\anyfolder\\anyname";
File targetFile = new File(file);
OutputStream outStream = new FileOutputStream(targetFile);
copyStream(cis, outStream);
cis.close();
outStream.close();
} catch (Throwable t) {
System.exit(1);
}
}
private static byte[] hexToBytes(String hex) {
...
return bytes;
}
private static void copyStream(InputStream in, OutputStream out) {
...
}
private static void sendPaymentOrder(float sum) {
...
}
}
But all attempts to hide the Activation Key by encryption are frustrated by the fact that both the key and the classes are decrypted in the JVM memory. The hacker can use a memory dump to get the data he needs. First, it uses the Java Tools API. Code below:
public class DumperAgent implements ClassFileTransformer {
public static void premain(String args, Instrumentation instr) {
agentmain(args, instr);
}
public static void agentmain(String agentArgs, Instrumentation instr) {
instr.addTransformer(new DumperAgent(), true);
Class<?>[] classes = instr.getAllLoadedClasses();
try {
instr.retransformClasses(classes);
}
catch (UnmodifiableClassException e) {}
}
public byte[] transform(ClassLoader loader, String className,
Class<?> redefinedClass, ProtectionDomain protDomain, byte[] classBytes) {
dumpClass(className, classBytes);
return null;
}
private static void dumpClass(String className, byte[] classBytes) {
try {
className = className.replace("/", File.separator);
// ...
FileOutputStream fos = new FileOutputStream(fileName);
fos.write(classBytes);
fos.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
And Attacher
:
public class Attacher {
private static String pathToAttacherJar, pid;
public static void main(String[] args) throws Exception {
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(pathToAttacherJar, null);
}
}
Unfortunately, this attempt was also unsuccessful. A simple analysis of the Java run time arguments detects unwanted arguments, and the program stops working upon initialization without having time to load anything.
static {
RuntimeMXBean mxBean = ManagementFactory.getRuntimeMXBean();
if (mxBean.getInputArguments().contains("-XX:-DisableAttachMechanism") ||
mxBean.getInputArguments().contains("-javaagent:")) {
System.exit(1);
}
}
This fragment should be placed in the first class, which usually contains the main(String[] args)
method.
What Can You Do?
The first thing you need to do is learn to see through the eyes of a hacker. This is identifying the architecture that your program is part of, looking for existing vulnerabilities, as well as possible vulnerabilities in your program and how to hack them.
Remember that you can always find protection from any attack. Remember also that there is a “Steal Once, Sell Many” attack, but there is also a “Protect Well, Protect for Long” defense. More hackers pose as "distributors" of cracked software than you might think.
Don't rely solely on obfuscators, servers, or cryptography. Protect only critical points and parts of programs. Use the structure of the program itself as it looks from the outside. Security should be as well designed as your program itself.
Afterword
- Code protection is a process, not a final result. New hacking methods are being invented, new versions of the JVM are being released, allowing more manipulation of the JVM memory, etc. This is the same war as between viruses and anti-virus.
- In other words, neither absolute weapons nor absolute protection exist and cannot exist. Any new method of attack gives rise to a corresponding method of defense that repels this attack.
- An intrusion detection process similar to the observer effect in quantum physics should be used in almost every case of security algorithm development.
- Any observation (intervention), reset, remote agent, etc., causes a disturbance in the observed environment, which can be noticed and protective measures taken.
- Thus, we have a classic example of the evolution of the attack-defense system. This means that for every offensive action, there is a defense, and vice versa. Note that the defense is always in the best position.
Opinions expressed by DZone contributors are their own.
Comments