Skip to content

Instantly share code, notes, and snippets.

@BenjaminUrquhart
Created March 3, 2021 04:21
Show Gist options
  • Save BenjaminUrquhart/0ab80d4c509354c8c8dee4804e5c7211 to your computer and use it in GitHub Desktop.
Save BenjaminUrquhart/0ab80d4c509354c8c8dee4804e5c7211 to your computer and use it in GitHub Desktop.
Small program to deobfuscate assets from RPG Maker games. Requires org.json.
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import javax.imageio.ImageIO;
import org.json.JSONObject;
public class RPGDump {
public static File folder;
public static Set<String> created = new HashSet<>();
public static void main(String[] args) throws Exception {
if(args.length == 0) {
Scanner sc = new Scanner(System.in);
System.out.print("Game folder: ");
folder = new File(sc.nextLine());
sc.close();
}
else {
folder = new File(String.join(" ", args));
}
if(!folder.exists()) {
throw new FileNotFoundException("File not found:" + folder.getAbsolutePath());
}
if(!folder.isDirectory()) {
throw new IllegalArgumentException("Provided path is not a folder: " + folder.getAbsolutePath());
}
if(new File(folder, "index.html").exists()) {
folder = folder.getParentFile();
}
File pkgInfo = new File(folder, "package.json");
if(!pkgInfo.exists()) {
throw new IllegalArgumentException("Not an RPG Maker game folder: " + folder.getAbsolutePath() + "\nMake sure you're providing the base folder that contains the game executable");
}
JSONObject json = new JSONObject(Files.readString(pkgInfo.toPath()));
System.out.println("Game: " + json.optString("name"));
List<File> files = getObfuscatedFiles(folder);
if(files.isEmpty()) {
System.out.println("No obfuscated assets found!");
return;
}
else {
System.out.println(files.size() + " obfuscated file(s) found.");
}
File system = new File(folder, "www/data/System.json");
String key = null;
byte[] keyBytes = new byte[16], bytes;
if(system.exists()) {
json = new JSONObject(Files.readString(system.toPath()));
key = json.optString("encryptionKey", null);
for(int i = 0; i < 32; i+=2) {
keyBytes[i/2] = (byte)Integer.parseInt(key.substring(i, i+2), 16);
}
}
else {
System.out.println("Could not find System.json!");
}
if(key == null) {
System.out.println("No key found!\nAttempting to brute-force...");
/* This is a known-plaintext attack against the sprites.
* We know this is a PNG file with the first chunk being
* IHDR. This gives us 12 bytes of the 16-byte key by
* XORing our known data with the beginning of the file.
*
* The only thing we don't know is the length of the IHDR
* chunk, but we can brute-force this. It shouldn't be that
* large. Once we find the length, we have the key.
*/
// Find the smallest PNG. This way, we can try to minimize the time
// spent brute-forcing the length.
System.out.println("Finding a suitable PNG to attack...");
File candidate = files.stream()
.filter(f -> f.getName().endsWith(".rpgmvp"))
.sorted((a,b) -> (int)(a.length() - b.length()))
.findFirst()
.orElse(null);
if(candidate == null) {
// No images found, I'm lazy and don't want to do this on oggs.
System.out.println("Could not find an image to brute-force the key with!");
return;
}
long start = System.currentTimeMillis();
System.out.println("Target file: " + candidate.getAbsolutePath());
bytes = Files.readAllBytes(candidate.toPath());
ByteBuffer buff = ByteBuffer.wrap(bytes), keyBuff = ByteBuffer.wrap(keyBytes);
buff.order(ByteOrder.BIG_ENDIAN);
long signature = buff.getLong();
long other = buff.getLong();
long ver = other >> 40;
long rem = other & ((1L << 40) - 1);
if(signature != 0x5250474d56000000L) {
throw new IllegalStateException("Not an RPG Maker obfuscated file.");
}
buff.position(0);
StringBuilder sb = new StringBuilder();
byte b = buff.get();
while(b != 0) {
sb.append((char)b);
b = buff.get();
}
System.out.printf("Header: %08x%08x (%s v%d, r%d)\n", signature, other, sb, ver, rem);
bytes = Arrays.copyOfRange(bytes, 16, bytes.length);
buff = ByteBuffer.wrap(bytes);
System.out.println("Trimmed size: " + bytes.length + " bytes");
keyBuff.order(ByteOrder.BIG_ENDIAN);
buff.order(ByteOrder.BIG_ENDIAN);
keyBuff.position(0);
buff.position(0);
// Known PNG header segments
final int HEADER_1 = 0x89504e47, HEADER_2 = 0x0d0a1a0a, HEADER_4 = 0x49484452;
keyBuff.mark();
buff.mark();
// Prepare for brute-force
keyBuff.putInt(buff.getInt() ^ HEADER_1);
keyBuff.putInt(buff.getInt() ^ HEADER_2);
keyBuff.putInt(12, buff.getInt(12) ^ HEADER_4);
buff.reset();
buff.putInt(HEADER_1);
buff.putInt(HEADER_2);
buff.putInt(12, HEADER_4);
InputStream stream = new ByteArrayInputStream(bytes);
stream.mark(bytes.length + 1);
int length = buff.getInt(8);
int realLen = 1;
// Check for overflow
while(realLen > 0) {
try {
if(realLen % 1000 == 0) {
System.out.println(realLen);
}
// Test length
buff.putInt(8, realLen);
ImageIO.write(ImageIO.read(stream), "png", new File("test.png"));
// Found correct length
break;
}
catch(Exception e) {
// Try again
realLen++;
}
finally {
stream.reset();
}
}
key = "";
stream.close();
// Write last part of the key
keyBuff.putInt(8, realLen ^ length);
for(int i = 0; i < keyBytes.length; i++) {
key += Integer.toHexString(keyBytes[i] & 0xff);
}
System.out.println("Found key in " + (System.currentTimeMillis() - start) + "ms (Took " + realLen + " attempts)");
}
System.out.println("Key: " + key);
int index = 1;
for(File file : files) {
try {
if(index % 100 == 0) {
System.out.printf("Processing %d / %d (%.2f%%)\n", index, files.size(), (index / (double) files.size()) * 100);
}
bytes = getTrimmedFile(file);
// Only the first 16 bytes of the file are XORed
// because yes.
for(int i = 0; i < keyBytes.length; i++) {
bytes[i] ^= keyBytes[i];
}
Files.write(getNormalFile(file).toPath(), bytes);
}
catch(Exception e) {
e.printStackTrace();
}
index++;
}
System.out.println("Done. Deobfuscated files are in " + new File(folder, "output").getAbsolutePath());
}
public static File getNormalFile(File file) {
String name = file.getName();
if(!name.contains(".")) {
throw new IllegalArgumentException("File does not have an extension");
}
int last = name.lastIndexOf(".");
String path = file.getParent() + "/";
String root = folder.getAbsolutePath();
if(path.startsWith(root)) {
path = root + "/" + path.substring(root.length() + 1).replaceFirst("www", "output");
if(created.add(path)) {
new File(path).mkdirs();
}
}
path += name.substring(0, last);
String ext = name.substring(last + 1);
switch(ext) {
case "rpgmvp": return new File(path + ".png");
case "rpgmvo": return new File(path + ".ogg");
default: throw new IllegalArgumentException("Invalid extension: " + ext);
}
}
public static byte[] getTrimmedFile(File file) throws IOException {
byte[] bytes = Files.readAllBytes(file.toPath());
return Arrays.copyOfRange(bytes, 16, bytes.length);
}
public static List<File> getObfuscatedFiles(File folder) {
List<File> out = new ArrayList<>();
String name;
for(File file : folder.listFiles()) {
if(file.isDirectory()) {
out.addAll(getObfuscatedFiles(file));
}
else {
name = file.getName();
if(name.endsWith(".rpgmvp") || name.endsWith(".rpgmvo")) {
out.add(file);
}
}
}
return out;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment