andrew kim / projects / game modding

i mod games for fun. rpg maker games ship their scripts, maps, and art inside a single encrypted archive, so to get at them i wrote ruby tools that unpack the two formats xp and vx ace use (rgssad v1 and v3).

the encryption

both formats xor the data in 4-byte blocks against a running key, and the key steps forward by k * 7 + 3 after each block. xp starts the key at a fixed 0xdeadcafe, while vx ace derives it from a seed stored in the archive header. that's the only real difference between the two. once you know the schedule, the same routine decrypts the file table and every file in it.

$ xxd -l 48 Game.rgss3a
00000000: 5247 5353 4144 0003 3412 ed5e 10a3 5556  RGSSAD..4..^..UV
00000010: b6a8 5556 aac9 a14c c3a3 5556 93c2 2137  ..UV...L..UV..!7
00000020: 8bf0 3624 bed3 2125 f9d1 2332 b6d7 3464  ..6$..!%..#2..4d

$ ruby rgss3a_extract.rb Game.rgss3a extracted
  -> Data\Scripts.rvdata2 (2913 bytes)
  -> Data\System.rvdata2 (1406 bytes)
  -> Data\Map001.rvdata2 (3720 bytes)
  -> Graphics\Titles1\Title.png (4096 bytes)
  -> Audio\BGM\Theme.ogg (5120 bytes)
scanned 5, extracted 5
the extractor running on a test archive
bin = File.binread(src)
abort "not RGSSAD v3" unless bin[0, 8] == "RGSSAD\0\x03".b

seed = bin[8, 4].unpack1('V')
key = (seed * 9 + 3) & 0xFFFFFFFF
pos = 12

advance = ->(k) { (k * 7 + 3) & 0xFFFFFFFF }

decrypt_data = lambda do |bytes, fkey|
  out = String.new(capacity: bytes.bytesize)
  k = fkey
  i = 0
  while i < bytes.bytesize
    take = [4, bytes.bytesize - i].min
    take.times do |j|
      out << ((bytes.getbyte(i + j) ^ ((k >> (j * 8)) & 0xFF)) & 0xFF).chr
    end
    k = advance.call(k)
    i += take
  end
  out
end

count = 0
extracted = 0
while pos < bin.bytesize
  offset = bin[pos, 4].unpack1('V') ^ key; pos += 4
  break if offset == 0
  size     = bin[pos, 4].unpack1('V') ^ key; pos += 4
  fkey     = bin[pos, 4].unpack1('V') ^ key; pos += 4
  name_len = bin[pos, 4].unpack1('V') ^ key; pos += 4
  name_bytes = bin[pos, name_len].bytes; pos += name_len
  key_bytes = [key].pack('V').bytes
  name = name_bytes.each_with_index.map { |b, i| (b ^ key_bytes[i % 4]).chr }.join

games that don't pack like that

plenty of games just keep their values in memory while they run. for those i use cheat engine. scan for a value, change it in game, scan again for the new value, and repeat until only the real address is left. then i can set it to whatever i want.

download the extractors (2 kb zip)