Created
March 3, 2021 04:21
-
-
Save BenjaminUrquhart/0ab80d4c509354c8c8dee4804e5c7211 to your computer and use it in GitHub Desktop.
Small program to deobfuscate assets from RPG Maker games. Requires org.json.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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