sebbu

description of a game save editor

Sep 6th, 2026
125
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 5.85 KB | None | 0 0
  1. The developer Millhaven didn't want players to simply open their saves in a text editor and jack up their money, relationships, and unlocked scenes. Therefore, each save is signed with HMAC-SHA256 (separately for the header and body), and SaveManager verifies the signature upon loading. If a save has been manually changed, the signature won't match, and the resulting download is marked as TAMPERED. The HMAC key is hardcoded into the game itself and is the same for all copies—meaning it's protection against "manual file editing," not a cryptographically secure system.
  2.  
  3. How MillhavenSaveEditor.exe works
  4. The idea is simple: don't manually touch the binary save format, but force the game to save it correctly, bypassing this protection not by hacking, but by using the game's actual code. Three layers:
  5.  
  6. 1. Launching the game under Frida
  7. When MillhavenSaveEditor.exe (the Backend class in MillhavenSaveEditor.py) starts, it calls frida.get_local_device().spawn([Millhaven.exe]) - this starts the game in a suspended state, attaches to the process (device.attach(pid)) and loads a JS script into it (embedded in MillhavenSaveEditor.py as the JS_TEMPLATE string; originally, this was a separate hook_editor.js). Then it calls device.resume(pid) - the game starts with the hook already embedded.
  8.  
  9. 2. Injecting Python code into the game process
  10. The JS script waits for python313.dll to appear in memory (Nuitka uses real CPython internally), retrieves the exported C API functions from it—PyGILState_Ensure, PyGILState_Release, PyRun_SimpleString—and, 4 seconds after startup (to allow the game to create its objects), executes arbitrary Python code directly in the game's address space. This is a standard trick for injecting into a live CPython process (as done by pyrasite).
  11.  
  12. Injected code:
  13.  
  14. Looks for a live SaveManager instance via gc.get_objects() (iterates through all objects in the Python heap and takes the one with type(o).__name__ == 'SaveManager')
  15. Starts a background thread (threading.Thread) that checks the request file (save_req.json in the temporary folder) once a second and, if it has changed, performs the action and writes the response to save_resp.json
  16. 3. File sharing - the GUI itself
  17. The Tkinter window knows nothing about the game's internals - it simply:
  18.  
  19. writes {"action": "dump", "slot": N, "kind": "manual"} -> waits for a response -> displays the JSON in the text field
  20. Pressing the Save button takes the edited JSON from the text field -> writes {"action": "write", "slot": N, "data": {...}} -> waits {"ok": true}
  21. Important: both dump and write within the injected code call the real sm.load_game(...) / sm.save_game(...) methods – the same ones used by the game itself. This means:
  22.  
  23. load_game itself decrypts the envelope, verifies both HMACs, and returns the completed SaveGameData
  24. save_game itself serializes, compresses, and recalculates the HMAC using the same key as the game – so the edited save will never be TAMPERED/CORRUPT, and CheatCounter won't touch it.
  25. In essence, this isn't "hacking the save format," but rather a remote control for the game's real code – all the cryptographic logic remains within the game itself, the GUI only pulls its strings via file IPC.
  26.  
  27. If you don't want to edit through the app, you can do it this way. The entire save-editor source code is already in the folder, and you can use it. However, you'll need to install Python and pip to do this. Next,
  28. how to use (repeat each time):
  29.  
  30. 0) Install Python - https://www.python.org/downloads/
  31. 0.1) Pip will install with Python
  32. 1) Open the save-editor folder in the game directory.
  33. 2) Follow the screenshots.
  34. 3) Hover over the zone from the first screenshot and click on it
  35. ааааааааимени.png
  36. 4) Erase everything and type cmd. Press enter
  37. ааааааааимени.png
  38. 5) A terminal will open in this folder.
  39. 123.png
  40. 6) Install the dependencies (* Explanation of why this is necessary is below.) in the terminal once: pip install frida frida-tools
  41. 7) Run in the terminal: python run_hook.py - this will launch the game and after about 5 seconds, start a background editor thread in the process (the file editor_ready.json will appear)
  42. 8) Get the save file as JSON in the terminal: python save_editor.py dump 1 manual slot1.json
  43. 9) Open slot1.json (which will appear in the save-editor folder) in any text editor and change what you need - for example, economy.money, character_states.yuki.affection, skills.levels.persuasion, unlocked_scenes, etc.
  44. 10) Write back in the terminal: python save_editor.py write 1 manual slot1.json
  45. 11) Load the save in the game as usual – verified live (round-trip test on the real save_01.sav): outcome: "ok", no TAMPERED.
  46.  
  47. kind - manual (slots 1–16) or auto (autosaves 1–3). As long as run_hook.py is running, you can dump/write as many times as you like.
  48.  
  49.  
  50. *
  51. frida is the core dynamic instrumentation library. It is a Python wrapper around frida-core. Can:
  52.  
  53. - Launch a suspended process (spawn) or attach to an already running one (attach)
  54. - Inject JS scripts into it, which are executed within the process itself via the built-in QuickJS/V8 engine
  55. - From this JS script, read/write process memory, intercept function calls (Interceptor.attach), find exported symbols in loaded DLLs (Module.findExportByName), and call arbitrary native functions (NativeFunction)
  56.  
  57. We used frida to:
  58.  
  59. - Intercept the PKCS5_PBKDF2_HMAC call in libcrypto-3.dll and extract the HMAC key for saves
  60. - Inject Python code via PyRun_SimpleString to trigger the game's actual SaveManager
  61.  
  62. frida-tools — an add-on with ready-made CLI utilities on top of frida (frida, frida-ps, frida-trace, frida-discover, etc.) for interactive work from the terminal—process listing, function call tracing, and the like.
  63.  
  64. Simply put: frida is a "hook into someone else's process and do whatever you want there" engine, and frida-tools is a set of ready-made console commands on top of it.
Add Comment
Please, Sign In to add comment