Alright. If you don’t mind I’ll post a tiny bit of the decompiled code to explain my reasoning. (apologies for the long post btw)
In the 1.7.2 SCELite client I happened upon this:
private static /* synthetic */ String o(String a) { // hash(String filePath)
int a2;
MessageDigest a3 = null;
try {
a3 = MessageDigest.getInstance(biv.l("\u0019P\u0000*l+q")); // SHA-256
} catch (NoSuchAlgorithmException a) {
a.printStackTrace();
}
[...]
StringBuffer a8 = new StringBuffer();
int n = a2 = 0;
while (n < a7.length) {
String string = Integer.toString((a7[a2] & 255) + 256, 16);
a8.append(string.substring(1));
n = ++a2;
}
return a8.toString();
}
The function just returns the SHA-256 hash for the given file. The fact that it returns just the SHA-256 hash of the file for this is actually completely meaningless. It could be MD5 or XOR with some key and it’d still work. The important thing was that it was the method that had to be trusted.
So what’s the secret? Modify the JRE. We all trust the JRE, right? In this case, it’s significantly easier than actually modifying your obfuscated mess. I only modified a couple classes inside rt.jar
For example, bypassing the date restriction by modifying java.util.Date
:
public boolean after(Date when) {
if(getMillisOf(when) == 1398920400054L) {
return false;
}
return getMillisOf(this) > getMillisOf(when);
}
Yes, I could’ve changed my PC’s date most likely but that’s not as fun.
Now to actually bypass the check: note that StringBuffer
is being used to create the final result hash. So instead of modifying the client (and thus changing the hash), the class actually creating the string can tell me it to me …
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
String value = new String(toStringCache, true);
System.out.println(value);
return value;
}
Then I just re-ran the client to see what I get. Of course, you get a lot of strings but finding what I wanted was easy enough:
C:\Users\Hidendra\AppData\Roaming\.minecraft\versions\1.7.2-sce\1.7.2-sce.jar
2ccddca6223cfcc83b8f0229de15215f63c734cff7963e804cb2c7230a0888ee
bivh
bivh
8b287e2b9e9f1798ff3fbee2d8b9bacf575e3ab268beae03e84081853027ef87
bivh
bivl
bivl
bivl
[...]
(those strings prefixed with biv are the keys for the string decryption calls)
The first string – 2ccddca6223cfcc83b8f0229de15215f63c734cff7963e804cb2c7230a0888ee
– is actually the SHA-256 hash exactly for the file 1.7.2-sce.jar
. I didn’t really care about that (I didn’t even look at that until later) and blindly forged ahead: I then loaded up my illegal OptiFine client and saw what it spit out:
C:\Users\Hidendra\AppData\Roaming\.minecraft\versions\1.7.2-sce\1.7.2-sce.jar
7a39fdbc92c7d11de96fedb02c2941b47beda0bb93a077d16a5412466a9444af
bivh
bivh
8b287e2b9e9f1798ff3fbee2d8b9bacf575e3ab268beae03e84081853027ef87
bivh
bivl
[...]
Thus the first hash is the one that changes. All I had to do was make StringBuffer/toString()
return 2ccddca6223cfcc83b8f0229de15215f63c734cff7963e804cb2c7230a0888ee
when it sees 7a39fdbc92c7d11de96fedb02c2941b47beda0bb93a077d16a5412466a9444af
. Then it’s done. After that, I could log into the server with my illegal OptiFine client because as far as SCE knew, the hash was identical
The method was essentially the same with the 1.6.4
version. It seemed like it spit out a couple more hashes, but I did not really care about that and just blindly remapped them to what the clean client gave.
For my exact StringBuffer changes, I did make it a bit easier on myself, so here: https://cryptbin.com/aiU4T4#7279c882098bd430a24733e92a3af9ae (key = input string, value = what to return instead)
This could’ve been done in multiple other ways as well; I just took the easier of the ways.
I do like the idea though and completely agree that something like this is great for stopping common tools like you say.