ZenHAX

Free Game Research Forum | Official QuickBMS support | twitter @zenhax | SSL HTTPS://zenhax.com
It is currently Fri Aug 18, 2017 8:18 pm

All times are UTC




Post new topic  Reply to topic  [ 3 posts ] 
Author Message
PostPosted: Sat Jul 29, 2017 11:42 pm 

Joined: Wed Aug 13, 2014 6:43 pm
Posts: 14
Unlike on consoles, where the save files for these games are usually plain, on PC they are now compressed with a custom method that uses the YKCMP_V1 header, also seen in other NIS games. On top of that, they are obfuscated with a XOR operation on the whole thing.

I've written a script that gets rid of both the obfuscation as well as decompressing the save file. It works for save files from both Disgaea PC as well as Disgaea 2 PC.

In version 2 I implemented a kind of pseudo-compression which does not actually compress the file but in fact makes it bigger, but it at least allows us to re-pack a previously decompressed save into a format that the games accept. So it's now possible to load modified save games.

https://pastebin.com/R1ZHA6W2


Last edited by HenryEx on Wed Aug 09, 2017 5:29 pm, edited 1 time in total.

Top
   
PostPosted: Sat Jul 29, 2017 11:43 pm 

Joined: Wed Aug 13, 2014 6:43 pm
Posts: 14
Script V1:
Code:
# Disgaea PC / Disgaea 2 PC save game decompression
# De-XORs and decompresses save files from these games.
# Leaves headers intact, so actual save data starts at offset 0x44.
#
# Written by HenryEx
# version 1
#
# script for QuickBMS http://quickbms.aluigi.org

################################################
# set up virtual memory file for save data
  print "Preparing..."
  get FILENAME filename 0
  get FILESIZE asize 0
  log MEMORY_FILE 0 FILESIZE 0  # MEMORY_FILE is the working copy for decryption
  log MEMORY_FILE2 0 0  # MEMORY_FILE2 will be the target for decompression
  putvarchr MEMORY_FILE2 0x100000 0
  log MEMORY_FILE2 0 0  # reset MF2

################################################
# Decrypt save

  print "Starting Decryption..."
  getDString HEADSTART 0x20
  get XORKEY long
  get XORUNK1 short
  get XORUNK2 short
  get XORCHUNKS long  # num of XOR'd integers in file
  get FSIZE_COMP_A long  # savedata size with YKCMP header minus padding (for XOR ints)
  SavePos FSTART

  print "XORing Memory File..."
# XOR save file with key
  for i = 0 < XORCHUNKS
    xmath POS "(i * 4) + FSTART"  # get current file position

    getvarchr DATA MEMORY_FILE POS long
    math DATA u^ XORKEY
    putvarchr MEMORY_FILE POS DATA long
  next i

################################################
# Decompress save

  print "Start Decompression..."
  goto FSTART MEMORY_FILE
  idstring MEMORY_FILE "YKCMP_V1"
  get ARCHIVE_VERSION long MEMORY_FILE
  if ARCHIVE_VERSION != 4
    print "[!] Unexpected archive version: %ARCHIVE_VERSION%! Exiting..."
    CleanExit
  endif
  get FSIZE_COMP_B long MEMORY_FILE  # comp. filesize without 0x30 decryption header, minus padding
  get FSIZE_TARGET long MEMORY_FILE  # target filesize without all headers after decomp.
  SavePos FSTART

# set up decompression and prepare MF2
  print "Setting up Memory File 2..."
  putDString HEADSTART 0x20 MEMORY_FILE2
  put XORKEY long MEMORY_FILE2
  put XORUNK1 short MEMORY_FILE2
  put XORUNK2 short MEMORY_FILE2
  put XORCHUNKS long MEMORY_FILE2
  put FSIZE_COMP_A long MEMORY_FILE2
  putct "YKCMP_V1" string -1 MEMORY_FILE2
  put ARCHIVE_VERSION long MEMORY_FILE2
  put FSIZE_COMP_B long MEMORY_FILE2
  put FSIZE_TARGET long MEMORY_FILE2
  set FBYTES long FSIZE_COMP_B
  math FBYTES + 0x30  # num. of compressed bytes + XOR & YKCMP header

  math POS = FSTART  # offset after the YKCMP_V1 header (save data start) for MF2

# start decompressing into MF2
  print "Decompressing save data..."
  for i = 68 < FBYTES  # i works as byte offset, start at offset 0x44
    goto i MEMORY_FILE
    get A_BYTE byte MEMORY_FILE
   
    if A_BYTE >= 0xE0    # read data like XX XY YY
      get B_BYTE byte MEMORY_FILE
      get C_BYTE byte MEMORY_FILE
      set READLEN long A_BYTE
      math READLEN & 0x1F  # remove 0xE0 from X
      math READLEN < 4
      xmath READLEN "READLEN + (B_BYTE > 4)"
      math READLEN + 3
      set SEEKBACK long B_BYTE
      math SEEKBACK & 0x0F
      math SEEKBACK < 8
      math SEEKBACK + C_BYTE
      math SEEKBACK + 1
    # print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0xE0! Next bytes %B_BYTE|2h% %C_BYTE|2h%. Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
      math i + 2  # advance counter of processed bytes
    elif A_BYTE >= 0xC0  # read data like XX YY
      get B_BYTE byte MEMORY_FILE
      math READLEN = A_BYTE
      math READLEN & 0x3F  # remove 0xC0 from X
      math READLEN + 2
      math SEEKBACK = B_BYTE
      math SEEKBACK + 1
    # print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0xC0! Next byte %B_BYTE|2h%. Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
      math i + 1  # advance counter of processed bytes
    elif A_BYTE >= 0x80  # read data like XY
      math READLEN = A_BYTE
      math READLEN > 4
      math READLEN & 3  # remove 0x80 from X
      math READLEN + 1
      math SEEKBACK = A_BYTE
      math SEEKBACK & 0x0F
      math SEEKBACK + 1
    # print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0x80! Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
    else                 # byte is < 0x80, straight copy next bytes MF1 -> MF2
    # print "Offset %i|h4%: byte %A_BYTE|2h% is < 0x80! Straight copy %A_BYTE% bytes to %POS|6h%!"
      for j = 0 < A_BYTE
        math i + 1
        getvarchr DATA MEMORY_FILE1 i byte
        put DATA byte MEMORY_FILE2
      next j
    endif

    if A_BYTE >= 0x80  # Copy bytes within MF2 via lookback

      math POS - SEEKBACK

      for j = 0 < READLEN
        getvarchr DATA MEMORY_FILE2 POS byte
        put DATA byte MEMORY_FILE2
        math POS + 1
      next j
    endif

    get POS asize MEMORY_FILE2
  next i

# check if filesize matches?
  math ENDSIZE = POS
  math POS - 68
  if FSIZE_TARGET != POS
    print "WARNING! Target filesize doesn't match real filesize!"
  endif
 
################################################
# Save file to disk

string FILENAME P= "dec_%FILENAME%"
print "Exporting decompressed save to %FILENAME%"
log FILENAME 0 ENDSIZE MEMORY_FILE2

CleanExit


Top
   
PostPosted: Wed Aug 09, 2017 5:31 pm 

Joined: Wed Aug 13, 2014 6:43 pm
Posts: 14
Script V2, supporting re-packing saves:
Code:
# Disgaea PC / Disgaea 2 PC save game decompression
# De-XORs and decompresses save files from these games.
# Leaves headers intact, so actual save data starts at offset 0x44.
#
# Saves decompressed with this script can be repacked as well with
# pseudo-compression to allow loading modified save files.
#
# Written by HenryEx
# version 2
#
# script for QuickBMS http://quickbms.aluigi.org

################################################
# set up virtual memory file for save data
  print "Preparing..."
  get FILENAME filename 0
  get FILESIZE asize 0
  if FILESIZE < 0x44
    print "[!] Error: File too small! Exiting..."
    CleanExit
  endif
  log MEMORY_FILE 0 0  # MEMORY_FILE is the working copy for de-/encryption
  log MEMORY_FILE2 0 0  # MEMORY_FILE2 will be the target for de-/recompression
  putvarchr MEMORY_FILE 0x100000 0
  putvarchr MEMORY_FILE2 0x100000 0
  log MEMORY_FILE 0 0  # reset MF1
  log MEMORY_FILE2 0 0  # reset MF2

  getDString HEADSTART 0x20
  get XORKEY long
  get XORUNK1 short
  get XORUNK2 short
  get XORCHUNKS long  # num of XOR'd integers in file
  get FSIZE_COMP_A long  # savedata size with YKCMP header minus padding (for XOR ints)
  SavePos FSTART 0
  getDString MAGIC 8 0   # check for YKCMP_V1 string if already de-crypted / -compressed

if MAGIC = "YKCMP_V1"
################################################
# Pseudo re-compress save
  print "File seems to be a decompressed save, will attempt to pseudo re-compress and encrypt it."

  get ARCHIVE_VERSION long 0
  if ARCHIVE_VERSION != 4
    print "[!] Unexpected archive version: %ARCHIVE_VERSION%! Exiting..."
    CleanExit
  endif
  get FSIZE_COMP_B long 0  # comp. filesize without 0x30 decryption header, minus padding
  get FSIZE_TARGET long 0  # target filesize without all headers after decomp.
  SavePos FSTART 0

# set up compression stuff and prepare MF2
  print "Setting up file in memory..."
  putct "YKCMP_V1" string -1 MEMORY_FILE2
  put ARCHIVE_VERSION long MEMORY_FILE2
  put FSIZE_COMP_B long MEMORY_FILE2  # place holder, needs to be updated after recompression
  math FSIZE_TARGET = FILESIZE
  math FSIZE_TARGET - 0x44
  put FSIZE_TARGET long MEMORY_FILE2
  set FBYTES long FSIZE_TARGET
  math POS = FSTART  # starting position to read bytes from, should be 0x44

# start pseudo compressing into MF2
  print "Pseudo re-compressing save data..."
  append  # append mode ON
  for FBYTES = FBYTES != 0  # loop while num of bytes to process is not 0
    if FBYTES > 0x7F
      set READLEN byte 0x7F
      math FBYTES - 0x7F
    else
      set READLEN byte FBYTES
      math FBYTES = 0
    endif

    put READLEN byte MEMORY_FILE2   # put byte length to straight copy
    log MEMORY_FILE2 POS READLEN 0  # append [READLEN] bytes to MF2
    math POS + READLEN              # increment read offset
  next
  append  # append mode OFF

  get FSIZE_COMP_B asize MEMORY_FILE2
  math FSIZE_COMP_A = FSIZE_COMP_B
  putvarchr MEMORY_FILE2 0xC FSIZE_COMP_B long  # update header value

################################################
# Encrypt save file in MF1

  print "Setting up encryption..."
  xmath PAD "4 - ( FSIZE_COMP_A % 4 )"  # num of bytes for padding
  if PAD > 0
    for i = 0 < PAD
      put 0 byte MEMORY_FILE2  # pad file with 0 for 32-bit alignment
    next i
  endif
  xmath FSIZE "FSIZE_COMP_A + PAD"
  xmath XORCHUNKS "FSIZE / 4"
# print "Padding needed: %PAD%. Padded filesize is %FSIZE%. Xor chunks: %XORCHUNKS%."

# Set up MF1
  putDString HEADSTART 0x20 MEMORY_FILE
  put XORKEY long MEMORY_FILE
  put XORUNK1 short MEMORY_FILE
  put XORUNK2 short MEMORY_FILE
  put XORCHUNKS long MEMORY_FILE
  put FSIZE_COMP_A long MEMORY_FILE
  SavePos FSTART MEMORY_FILE
  append  # append mode ON
  log MEMORY_FILE 0 FSIZE MEMORY_FILE2  # put recomp. save after encryption header
  append  # append mode OFF

  print "Encrypting File..."
# XOR save file with key
  for i = 0 < XORCHUNKS
    xmath POS "(i * 4) + FSTART"  # get current file position

    getvarchr DATA MEMORY_FILE POS long
    math DATA u^ XORKEY
    putvarchr MEMORY_FILE POS DATA long
  next i

  get ENDSIZE asize MEMORY_FILE

################################################
# Save file to disk

  string FILENAME $ "save"  # last occurrence + searched string
  print "Exporting re-compressed save to %FILENAME%"
  log FILENAME 0 ENDSIZE MEMORY_FILE

  CleanExit

else
################################################
# Decrypt save

  print "Decrypting File..."
# XOR save file with key
  log MEMORY_FILE 0 FILESIZE 0
  for i = 0 < XORCHUNKS
    xmath POS "(i * 4) + FSTART"  # get current file position

    getvarchr DATA MEMORY_FILE POS long
    math DATA u^ XORKEY
    putvarchr MEMORY_FILE POS DATA long
  next i

  goto FSTART MEMORY_FILE
  getDString MAGIC 8 MEMORY_FILE
  if MAGIC != "YKCMP_V1"
    print "[!] Unexpected magic string: %MAGIC%! Decryption might have failed. Exiting..."
    CleanExit
  endif


################################################
# Decompress save

  print "Begin Decompression..."
  get ARCHIVE_VERSION long MEMORY_FILE
  if ARCHIVE_VERSION != 4
    print "[!] Unexpected archive version: %ARCHIVE_VERSION%! Exiting..."
    CleanExit
  endif
  get FSIZE_COMP_B long MEMORY_FILE  # comp. filesize without 0x30 decryption header, minus padding
  get FSIZE_TARGET long MEMORY_FILE  # target filesize without all headers after decomp.
  SavePos FSTART 0

# set up decompression and prepare MF2
  print "Setting up file in memory..."
  putDString HEADSTART 0x20 MEMORY_FILE2
  put XORKEY long MEMORY_FILE2
  put XORUNK1 short MEMORY_FILE2
  put XORUNK2 short MEMORY_FILE2
  put XORCHUNKS long MEMORY_FILE2
  put FSIZE_COMP_A long MEMORY_FILE2
  putct "YKCMP_V1" string -1 MEMORY_FILE2
  put ARCHIVE_VERSION long MEMORY_FILE2
  put FSIZE_COMP_B long MEMORY_FILE2
  put FSIZE_TARGET long MEMORY_FILE2
  set FBYTES long FSIZE_COMP_B
  math FBYTES + 0x30  # num. of compressed bytes + XOR & YKCMP header

  math POS = FSTART  # offset after the YKCMP_V1 header (save data start) for MF2

# start decompressing into MF2
  print "Decompressing save data..."
  for i = 68 < FBYTES  # i works as byte offset, start at offset 0x44
    goto i MEMORY_FILE
    get A_BYTE byte MEMORY_FILE
   
    if A_BYTE >= 0xE0    # read data like XX XY YY
      get B_BYTE byte MEMORY_FILE
      get C_BYTE byte MEMORY_FILE
      set READLEN long A_BYTE
      math READLEN & 0x1F  # remove 0xE0 from X
      math READLEN < 4
      xmath READLEN "READLEN + (B_BYTE > 4)"
      math READLEN + 3
      set SEEKBACK long B_BYTE
      math SEEKBACK & 0x0F
      math SEEKBACK < 8
      math SEEKBACK + C_BYTE
      math SEEKBACK + 1
    # print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0xE0! Next bytes %B_BYTE|2h% %C_BYTE|2h%. Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
      math i + 2  # advance counter of processed bytes
    elif A_BYTE >= 0xC0  # read data like XX YY
      get B_BYTE byte MEMORY_FILE
      math READLEN = A_BYTE
      math READLEN & 0x3F  # remove 0xC0 from X
      math READLEN + 2
      math SEEKBACK = B_BYTE
      math SEEKBACK + 1
    # print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0xC0! Next byte %B_BYTE|2h%. Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
      math i + 1  # advance counter of processed bytes
    elif A_BYTE >= 0x80  # read data like XY
      math READLEN = A_BYTE
      math READLEN > 4
      math READLEN & 3  # remove 0x80 from X
      math READLEN + 1
      math SEEKBACK = A_BYTE
      math SEEKBACK & 0x0F
      math SEEKBACK + 1
    # print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0x80! Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
    else                 # byte is < 0x80, straight copy next bytes MF1 -> MF2
    # print "Offset %i|h4%: byte %A_BYTE|2h% is < 0x80! Straight copy %A_BYTE% bytes to %POS|6h%!"
      for j = 0 < A_BYTE
        math i + 1
        getvarchr DATA MEMORY_FILE1 i byte
        put DATA byte MEMORY_FILE2
      next j
    endif

    if A_BYTE >= 0x80  # Copy bytes within MF2 via lookback

      math POS - SEEKBACK

      for j = 0 < READLEN
        getvarchr DATA MEMORY_FILE2 POS byte
        put DATA byte MEMORY_FILE2
        math POS + 1
      next j
    endif

    get POS asize MEMORY_FILE2
  next i

# check if filesize matches?
  math ENDSIZE = POS
  math POS - 68
  if FSIZE_TARGET != POS
    print "WARNING! Target filesize doesn't match real filesize!"
  endif
 
################################################
# Save file to disk

  string FILENAME P= "dec_%FILENAME%"
  print "Exporting decompressed save to %FILENAME%"
  log FILENAME 0 ENDSIZE MEMORY_FILE2

  CleanExit

endif


Top
   
Display posts from previous:  Sort by  
Post new topic  Reply to topic  [ 3 posts ] 

All times are UTC


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Powered by phpBB® Forum Software © phpBB Limited