So it looks like someone posted source for decrypting save files and text assets over on the chaosbane subreddit:
Code: Select all
https://www.reddit.com/r/Chaosbane/comments/bwupx7/blood_for_the_blood_god/
figured I'd post this here as well so that someone can make use of it instead of being limited to some random Chinese binary. It contains a detailed description of how the saves and ynm assets are encrypted, and provides a simple proof of concept source to decrypting/re-encrypting them.
Edit: Somehow the link keeps screwing up, so I just put it in a code block.
Edit 2:
To save the post for future use, in case it gets removed:
So the save game format intrigued me. The save games, and the YNM text assets, are both encrypted with a fairly simple algo. This really only took me about a half day to figure out and write a quick proof of concept for.
Take the name of the file, remove the extension, and make it lowercase
Append EKO_SECRET:7373730! to the filename, ie: savecharf_public_slot0EKO_SECRET:7373730!
Pass the key string through an RC4 prepare_key function that results in a 256 byte key
Create a zeroed buffer the size of the input, and run the buffer and the new RC4 key through the RC4 algo
Use this 1:1 buffer and xor the input
Repeat the process to re-encrypt the file.
As I said, this will get you into both the saves, and the other ynm text assets. Possibly more, I didn't spend all that much time doing this and got bored after figuring out the encryption, that was the fun for me.
Save games are in <Steam Path>/steamapps/common/Warhammer Chaosbane/Data/Save as is some other files, like Common.ynm which contains your fragments and gold. Extra skill points can also be obtained by changing or adding the line <skills additionalPoints="1234"/> in common.ynm . You can more easily get to the game data folder by right clicking the game in steam, going to properties, Local Files tab, and hitting Browse Local Files.
Using this you can easily make extremely overpowered gear, as well. 100% cooldown, crit, crit damage, etc no problem. The files are just your run of the mill XML once you decrypt them. Remember to re-encrypt when you're done, the same goes for assets.
Here's an example of what you can do:
Here is the source for anybody who cares to make anything fun with this, I simply threw it together as a proof of concept and manually alter it to do what I want, I have no interest in making a full-blown save editor with this, someone else can have fun with that:
This was compiled using MSVC using C++17 standard, though should work in other stuff compatible with the C++17 standard.
Code: Select all
#include <iostream>
#include <string>
#include <filesystem>
#include <vector>
#include <fstream>
using namespace std;
namespace fs = filesystem;
struct rc4_key
{
uint8_t state[256]{ 0 };
// uint8_t x; // Not used
// uint8_t y; // Not used
};
void decrypt_or_encrypt_file(fs::path path, fs::path destination);
void cb_prep_key(fs::path path, rc4_key* key);
void init_key(rc4_key* out_key, const char* key, size_t key_size);
void rc4(rc4_key* key, uint8_t* block, size_t length);
void swap(uint8_t* a, uint8_t* b);
vector<uint8_t> read_bytes(fs::path file);
void write_bytes(fs::path path, uint8_t* data, size_t size);
constexpr char hard_key[] = "EKO_SECRET:7373730!";
constexpr char root_path[] = "<path to Steam>/steamapps/common/Warhammer Chaosbane/Data/Save";
constexpr char data_path[] = "<path to Steam>/steamapps/common/Warhammer Chaosbane/Data";
constexpr char extract_path[] = "./Path/To/Extract";
int main()
{
/*
The following will extract the data directory to a given path
*/
fs::path extract = extract_path;
for (auto& entry : fs::recursive_directory_iterator(data_path))
{
if (!entry.is_regular_file())
continue;
fs::path file = entry.path();
fs::path extract_path = extract / file.lexically_relative(data_path);
if (!fs::exists(extract_path.parent_path()))
fs::create_directories(extract_path.parent_path());
// Only decrypt ynm files, XML files are not encrypted.
if (file.extension() == ".ynm")
decrypt_or_encrypt_file(file, extract_path);
else
fs::copy(file, extract_path);
cout << extract_path.parent_path() << " (rel: " << entry.path().lexically_relative(data_path) << ")" << endl;
}
/*
The following will decrypt certain files in place.
*/
fs::path files[]
{
fs::path(root_path) / "SaveCharF_Public_slot0.ynm",
fs::path(root_path) / "SaveCharF_Public_slot1.ynm",
fs::path(root_path) / "common.ynm",
fs::path(data_path) / "Stats.ynm",
fs::path(data_path) / "Skills_Slayer.ynm",
fs::path(data_path) / "Skills_HighElf.ynm",
};
for (auto& file : files)
decrypt_or_encrypt_file(file, file);
return 0;
}
void decrypt_or_encrypt_file(fs::path path, fs::path destination)
{
rc4_key key;
cb_prep_key(path, &key);
auto file = read_bytes(path);
vector<uint8_t> xor_buffer(file.size());
rc4(&key, xor_buffer.data(), xor_buffer.size());
for (int i = 0; i < file.size(); i++)
file[i] = file[i] ^ xor_buffer[i];
write_bytes(destination.string(), file.data(), file.size());
}
void cb_prep_key(fs::path path, rc4_key* key)
{
std::string keystr = path.stem().string();
transform(keystr.begin(), keystr.end(), keystr.begin(), ::tolower);
keystr += hard_key;
init_key(key, keystr.c_str(), keystr.size());
}
void init_key(rc4_key* out_key, const char* key, size_t key_size)
{
int index1, index2;
uint8_t* state = out_key->state;
for (int i = 0; i < 256; i++)
state[i] = i;
index1 = index2 = 0;
for (int i = 0; i < 256; i++)
{
index2 = (key[index1] + state[i] + index2) % 256;
swap(&state[i], &state[index2]);
index1 = (index1 + 1) % key_size;
}
}
void rc4(rc4_key* key, uint8_t* block, size_t length)
{
int x = 0, y = 0;
uint8_t* state = key->state;
for (int i = 0; i < length; i++)
{
x = (x + 1) % 256;
y = (y + state[x]) % 256;
swap(&state[x], &state[y]);
int xorIndex = (state[x] + state[y]) % 256;
block[i] ^= state[xorIndex];
}
}
void swap(uint8_t* a, uint8_t* b)
{
uint8_t temp = *a;
*a = *b;
*b = temp;
}
std::vector<uint8_t> read_bytes(fs::path path)
{
ifstream file(path, ios::binary | ios::ate);
streampos size = file.tellg();
std::vector<uint8_t> result(size);
file.seekg(0, ios::beg);
file.read(reinterpret_cast<char*>(result.data()), size);
return result;
}
void write_bytes(fs::path path, uint8_t* data, size_t size)
{
ofstream file(path, ios::binary);
file.write(reinterpret_cast<const char*>(data), size);
file.close();
}