AWMA and I had an argument,
And I was determined to win it,
Yes I did, so three cheers for me,
Whoop, Whoop, Whoop!
While I was writing the previous installment of my adventures with AWMA, I spotted a bit of code that I hadn’t fully noticed the first time that I was skimming through the code. To be honest, I wasn’t really looking for it anyway. It’s shown below, and you may notice that is gives quite a big clue on how the licence code that you need to type is decoded.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static String decodeLicenseString(String str) { String news = ""; int[] xorcode = { 6, 3, 7, 4, 2, 9, 7, 5, 8, 6, 2, 5, 2, x, x, x, x }; for (int i = 0; i < str.length(); i++) { String s = str.substring(i, i + 1); int n = Integer.parseInt(s); int newn = 10 + n - xorcode[i]; newn %= 10; news = news + "" + newn; } // Snip... } |
That’s right… it gets the encoded license string, and for each number, adds 10 to it and then subtracts the relevant value in the xorcode array. This value is then modded against 10 (in other words gets the unit value of the number) and then adds it to a new string. Having this insight into how the code works, I decided to see exactly how far I could go. And armed with a few valid codes (from activating it so many times) I could test to see if any of them worked.
To start off with, I wrote down one of the activation codes, and then ran through this code line by line on a piece of paper. If you can’t read my writing (who can?!) then I’ve made a pretty table beneath the image for you.
License | 7 | 9 | 7 | 7 | 3 | 8 | 3 | 2 | 6 | 5 | 0 | 7 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
xorcode | 6 | 3 | 7 | 4 | 2 | 9 | 7 | 5 | 8 | 6 | 2 | 5 |
Decoded | 1 | 6 | 0 | 3 | 1 | 9 | 6 | 7 | 8 | 9 | 8 | 2 |
Knowing that this licence runs out in a years’ time, the first six characters that were decoded looked very much date like (160319 – 2016-03-19). This was confirmed to me by looking further through the code, where it splits this decoded string into an expiry date and a hash.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static Map<LicenseKey, String> parseLicenseString(String str) { String dec = decodeLicenseString(str); if (dec.length() == CODELEN) { Map<LicenseKey, String> ret = new HashMap(); ret.put(LicenseKey.EXPIRY, dec.substring(0, 6)); ret.put(LicenseKey.HASH, dec.substring(6, 6 + HASHLEN)); return ret; } // Snip... } |
Now all that was needed to see what created this hash code, so that it could be used. However, this is where I had to stop trying to work this out on paper and move over to a computer doing my work for me. Why? Well, this is the code that creates the hash:
1 2 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 39 40 |
public static String createHash(String expiry, String machine) { MessageDigest md = MessageDigest.getInstance("MD5"); String txt = "" + machine + "-" + expiry + "-random"; md.update(txt.getBytes()); byte[] res = md.digest(); // Snip... for (int i = 0; i < res.length; i++) { int x = res[i] & 0xFF; if (x < 16) { hash = hash + "0"; } // Snip... hash = hash + Integer.toString(x, 16); String hashbit = ""; for (int i = 0; i < HASHLEN; i++) { hashbit = hashbit + ('\000' + hash.charAt(i)) % 10; } return hashbit; // Snip... } public static String machineToNumber(String machine) { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(machine.getBytes()); byte[] res = md.digest(); // Snip... String hash = ""; for (int i = 0; i < res.length; i++) { int x = res[i] & 0xFF; hash = hash + Integer.toString(x); } // Snip... return hash.substring(0, 6); } public static String getMachineId() { String machineid = System.getProperty("user.home"); // Big snip (Note: The code located here doesn't seem to be used?)... return machineToNumber(machineid); } |
Although I did try and work out bitwise addition on paper, the working out of MD5 hashes was something I wasn’t prepared to do. Moving over to NetBeans, and creating a new Java project, I copied over the AWMA license code, and started to step through it. Checking through the license string being decoded (albeit with a different license code) and making sure that the results were correct meant that I hadn’t wasted the last hour or so.
Carrying on debugging the program, I eventually got to the code that creates the hash that I needed. This hash is worked out by getting a string of the user’s profile location, doing some calculations on it, and then getting the first six characters of it (this is why a license is needed for each user of the program). This section of the program shows four variables are being used. The expiry of the license, the hash stored in the license, the ID of the machine and the hash that is calculated.
This is all of the information that is needed to generate a valid license code. Sticking the relevant parts together (expiry and calculated hash) and reversing the license decoding steps (add each number with the xorcode and then mod it by 10) a code that looked valid was created.
Expiry | 1 | 6 | 0 | 3 | 2 | 3 | – | – | – | – | – | – |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Calculated hash | – | – | – | – | – | – | 0 | 5 | 4 | 2 | 9 | 2 |
xorcode | 6 | 3 | 7 | 4 | 2 | 9 | 7 | 5 | 8 | 6 | 2 | 5 |
Encoded | 7 | 9 | 7 | 7 | 4 | 2 | 7 | 0 | 2 | 8 | 1 | 7 |
Having this code in front of me, I fired up a copy of AWMA and typed in the code. I was prompted by this (ignore that the code doesn’t match what is above):
Feeling experimental, I set the date to be the last day in 2099 and put this into the AWMA activation screen.
While it works, when you launch AWMA next it will complain that the licence has expired. Still, being able to generate our licences without needing to contact Pearson each time is going to save us quite a bit of hassle. And considering this is the first time I’ve ever reverse engineered something, being able to do it in 2 hours I don’t think is too bad.
In conclusion, looks like I have won the argument with AWMA, and I now have a nice little nifty tool that can generate the licenses for me too, should I ever need any more.