Anyone that has played souls-like games before knows that they can get quite challenging, so why not make our lives a little easier by writing some cheats for Dark Souls 3.
In this post, I am going to start by covering basic cheat engine usage to extract information from the game. Since just running the cheats through cheat engine is boring, I will continue by translating the cheat table into a dll and write a dll-injector that we can use to inject the hack into the game.
The code for both the dll injector and the actual dll is written in Rust.
Let's start by setting up our cheat table. To do this we will use a tool called cheat engine. The tool will allow us to quickly scan the memory of a process and attempt to find values.
Since dying appears to be a common problem in Dark Souls, we might as well try to start by making that a little harder. Our first goal is to try and find where in memory the game stores our health once we start it up. Cheat engine will prove very useful for this task.
To ultimately find the address of where the health value is stored, we will start by scanning memory for the exact value of our current health using cheat engine. Health is commonly stored as a 4-byte integer in games, so we will look for a 4-byte segment in writeable memory with the value of our current health, in this case, 800.
In this case, cheat engine found 9576 values in memory with that value.
Chances are that one of these above values found by cheat engine is used to track our health. These are obviously way too many values to manually verify though, so instead, we will let an enemy hit us, thus reducing our health, and then run yet another scan on this set of 9576 values with our new health value. The new set of values will be much smaller since only memory regions that contained 800 on the previous scan, and 630 on the current scan will be kept in the list.
We can repeat this process a couple of times until only a small amount of values are left (preferable < 10) that we can now manually check. This ends up providing us with both the health value and the maximum health cap, thus allowing us to edit our health by changing this value.
Ok, so we now have the ability to edit our health value, and make ourselves basically unkillable (outside of falling off the world edges). There is still one major issue we need to tackle though. These values are stored in memory that is dynamically allocated while loading the game. This means that if we restart the game, all of these values that we previously found will suddenly be invalid. We could of course repeat the previous process and find the health value again, but doing so every time we restart the game will quickly become tedious.
Luckily for us, cheat engine has another useful feature to help us with that. We can right-click the previously retrieved pointer and select the option to run a pointer scan. This will generate a list of pointer chains that lead from the program base to our selected pointer.
A possible pointer-chain could look something like this: *(*(Program_base+0x2000)+0x80)
In this case, the list contains a couple hundred pointers. We however only need 1 working pointer chain that persists over program restarts. The easiest way to do this is to double-click a couple of chains from the list to add them to our table and restart the game. At this point, hopefully, one of the previously saved pointers is still valid.
Since only dealing with health is a little boring, we can repeat the previous process to find some more values. A small trick to speed this up a little is to manually inspect the memory region around the found health pointer. Chances are that other values pertaining to player stats are stored alongside the health.
After a bit of work we can construct the table shown below.
At this point, we could stop there and be happy that we can beat the game without even breaking a sweat. Having to deal with the cheat table though is not very convenient, especially if we would like to make the cheat publicly available. This would require every user to have cheat engine installed to use the cheat.
Instead, we can write a dll, inject it into the process upon startup, and use our cheats via a simple-to-use gui. Also, let's be honest, writing the hacks is much more interesting than playing the game anyways. The first step to making this work is to write a dll injector so that we can inject our dll into a running process.
You can see the code to achieve this below. Since we require winapi to accomplish this, the lines to load in all required modules almost match the entire code base for the dll-injector. First we set up a helper function: get_func_address. This function allows us to retrieve the address of an exported function from a dll of our choosing. We use this to retrieve the address of LoadLibraryA.
Next, let's get to the main part of the injector. The inject_dll function is supposed to be called from our script. We provide it with the process we are trying to inject a dll into, and the path to the dll we are trying to inject. Next we use OpenProcess to open our target via the specified pid. Note that this allows us access to an existing process and does not start a new process. Next we use VirtualAllocEx to allocate enough virtual memory within the process for our dll, and manually write it into that newly allocated space using WriteProcessMemory. Finally, we use the CreateRemoteThread winapi function to spawn a thread within the target process and run our dll.
use std::mem;
use std::ffi::CString;
use std::ptr::null_mut;
use winapi::shared::minwindef::{LPVOID, DWORD, FALSE};
use winapi::um::{
memoryapi::{VirtualAllocEx, WriteProcessMemory},
processthreadsapi::{OpenProcess, CreateRemoteThread},
libloaderapi::{GetModuleHandleA, GetProcAddress},
winnt::{
PROCESS_CREATE_THREAD,
PROCESS_VM_OPERATION,
PROCESS_VM_WRITE,
MEM_RESERVE,
MEM_COMMIT,
PAGE_READWRITE,
},
};
/// Get process address of specified function {func} in specified module {mod}
unsafe fn get_func_addr(module: &str, func: &str) -> u64 {
let module = CString::new(module).unwrap();
let func = CString::new(func).unwrap();
let module_handle = GetModuleHandleA(module.as_ptr());
GetProcAddress(module_handle, func.as_ptr()) as u64
}
/// Inject dll specified by {dll_path} into process with the pid of {pid}
pub unsafe fn inject_dll(pid: u32, dll_path: &str) -> Option<()> {
let load_lib_addr = get_func_addr("Kernel32.dll", "LoadLibraryA");
let dll_path = CString::new(dll_path).unwrap();
let dll_path_len = dll_path.as_bytes_with_nul().len();
let proc = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, pid);
let va_path = VirtualAllocEx(proc, null_mut(), dll_path_len, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(proc, va_path, dll_path.as_ptr() as LPVOID, dll_path_len, null_mut());
type ThreadStartRoutine = unsafe extern "system" fn(LPVOID) -> DWORD;
let start_routine: ThreadStartRoutine = mem::transmute(load_lib_addr);
CreateRemoteThread(proc, null_mut(), 0, Some(start_routine), va_path, 0, null_mut());
Some(())
}
Now that we have the dll-injector we can get back to working on the actual game. Our goal is to write dll that we can inject into the process, but before we do that we need to write out a short exe. This will act as a wrapper to call the dll-injector and set everything up for us.
This code is very simple. We use a dependency called sysinfo to interact with the running processes and retrieve the pid of the Dark Souls process. Next we call the inject_dll function we defined earlier to inject the dll.
use std::path::Path;
use sysinfo::{ProcessExt, System, SystemExt};
const DEBUG: bool = false;
/// Retrieves target pid, and uses it to inject selected DLL into target process
fn main() {
let dll_path = if DEBUG {
"target\\debug\\ds3_cheat.dll"
} else {
"target\\release\\ds3_cheat.dll"
};
let dll = Path::new(dll_path).canonicalize().unwrap().into_os_string().into_string().unwrap();
let pid = System::new_all().process_by_name("DarkSoulsIII")[0].pid() as u32;
println!("[+] PID: {}", pid);
unsafe {
if dll_injector::inject_dll(pid, &dll).is_none() {
println!("Error injecting dll {} into process with pid: {}", dll_path, pid);
}
}
}
Let's start by setting up the basic skeleton required for a dll. DllMain is the function that is initially called once the dll is loaded in. It verifies that the reason why it was called was that a new dll was attached, and proceeds to spawn a new thread to run the dll_attach_wrapper in. This wrapper just calls the entry_point function, passing along the base of the dll. The entry_point function is where the majority of the action will happen.
I am not exactly sure why this wrapper is necessary, but without it I was not able to get the dll to compile.
/// Entry point that gets called once the dll is injected into Dark Souls
unsafe fn entry_point(base: winapi::shared::minwindef::LPVOID) -> u32 {
0
}
/// Small wrapper for the entry point
unsafe extern "system" fn dll_attach_wrapper(base: winapi::shared::minwindef::LPVOID) -> u32 {
entry_point(base);
0
}
/// DllMain is the main function that gets called when the dll is first attached
/// It creates a new thread to run the hack in
#[no_mangle]
pub unsafe extern "stdcall" fn DllMain(hinst_dll: HINSTANCE, fdw_reason: DWORD,
_lpv_reserved: LPVOID) {
if fdw_reason == winapi::um::winnt::DLL_PROCESS_ATTACH {
winapi::um::processthreadsapi::CreateThread(std::ptr::null_mut(), 0,
Some(dll_attach_wrapper), hinst_dll as _, 0, std::ptr::null_mut());
}
}
Since we want to use a gui for this application, let's start with that. We could do this using winapi, but this is a tedious process, so I opted to instead use a crate that makes this a whole lot easier: fltk/. Using it we can easily create a new window and add buttons to it for our needs. In the below example we set up a window and add a `quit` button. Next, we register a callback closure that gets called when the button is pressed. This function takes care of closing the app, and cleaning up all remains of the dll. This is also where we use the passed in `base` parameter to deallocate the memory allocated for the dll.
We could also just quit the application using the `x` button, but not properly deallocating memory will lead to undefined behavior once our dll is gone and the game keeps running. Finally we start up the window using the run method. This spawns the window below.
/// Entry point that gets called once the dll is injected into Dark Souls
unsafe fn entry_point(base: winapi::shared::minwindef::LPVOID) -> u32 {
// Draw the gui window and create the buttons
let app = app::App::default();
let mut wind = Window::new(100, 100, 400, 500, "Dark Souls 3 Hack");
let mut quit_button = Button::new(360, 0, 40, 40, "Quit");
wind.set_color(Color::White);
wind.end();
wind.show();
// Set up the instructions that get executed whenever a button/field is used
quit_button.set_callback(move |_| {
app.quit();
wind.clear();
FreeLibraryAndExitThread(base as HMODULE, 0);
});
app.run().unwrap();
0
}
Let's think about what we want to do. Previously we used cheat engine to directly write to locations in memory. We now want to replicate this in our program. Since we are in a dll within the binary, we can just read/write memory without issues. We can set up 2 small helper functions to both read and write from memory.
The next thing we did through cheat engine was to bypass dynamic memory allocation via pointer chains. We could just dereference multiple pointers + offsets, but that is a messy solution and won't scale very well. Instead, we can just define another small helper function. We pass in an array of vectors and dereference an address for each offset. This allows us to dynamically retrieve the address at the end of the pointer chain and thus operate on the value we are interested in.
/// Use the offsets to traverse multi-level pointers starting at {ptr}
unsafe fn bypass_dma(ptr: usize, offsets: Vec<usize>) -> usize {
let mut addr = ptr;
for offset in offsets {
addr = *(addr as *const usize);
addr += offset;
}
addr
}
/// Write {val} to {ptr}, uses the {offsets} to traverse multi level pointers
unsafe fn write_mem(ptr: usize, val: u32, offsets: Vec<usize>) {
let addr = bypass_dma(ptr, offsets);
let mut val_vec = vec![0u8; 4];
val_vec.write_u32::<LittleEndian>(val).unwrap();
*(addr as *mut u32) = val;
}
/// Read value from {ptr}, uses the {offsets} to traverse multi level pointers
unsafe fn read_mem(ptr: usize, offsets: Vec<usize>) -> u32 {
let addr = bypass_dma(ptr, offsets);
*(addr as *mut u32)
}
Now that we have the helper functions set up, let's complete the hack. We start by using a combination of GetModuleInformation and GetCurrentProcess to get the base address of this process. We will need this address to bypass aslr in case it is enabled on the system.
Next, we'll start by adding 2 new buttons. The first one is a toggleable button that allows us to define a callback that gets called whenever we toggle the button On/Off. The next one is an input button that allows the user to pass in a value to the callback function. Additionally, we add 2 frames. These are basically just small text sections on the window.
Next, we'll add a callback function for each button. The input button takes in a number that the user provides and writes it to the location of the health value in memory using the pointer chain we previously found using cheat engine. We make sure to overwrite both the health value and the maximum health cap. The toggleable infinite health button instead just edits the frame to display the string "On" or "Off" to the user, sets a variable that indicates that infinite health is enabled, and toggles the actual button.
Finally, we add 2 idle buttons. In fltk, these are run whenever nothing else is being done, so basically multiple times a second. The first one is in charge of displaying the current health value to the console at all times by reading the memory at the health pointer. The second one checks if the infinite health variable is set and if so writes the value 9999 to the health location in memory, thus making it impossible to die by taking damage.
const HEALTHOFFSET : usize = 0x04768E78;
unsafe fn entry_point(base: winapi::shared::minwindef::LPVOID) -> u32 {
let HASINFHP = Rc::new(AtomicBool::new(false));
// Get program base address
let modname = CString::new("DarkSoulsIII.exe").unwrap();
let mut m_info: MODULEINFO = MODULEINFO {
EntryPoint: std::ptr::null_mut(),
SizeOfImage: 0,
lpBaseOfDll: std::ptr::null_mut(),
};
let size = mem::size_of::<MODULEINFO>() as u32;
GetModuleInformation(GetCurrentProcess(), GetModuleHandleA(
modname.as_c_str().as_ptr()), &mut m_info, size);
let base_addr = m_info.lpBaseOfDll as usize;
// Define a toggleable button that lets us maintain 9999 health
let mut inf_hp = ToggleButton::new(20, 100, 30, 30, "Off");
Frame::new(55, 100, 0, 30, "INFINITE HEALTH").with_align(Align::Right);
// Define a button that takes an input and sets the health to that value
let mut health = IntInput::new(20, 180, 30, 30, "");
let mut health_frame = Frame::new(55, 180, 0, 30, "").with_align(Align::Right);
health.set_callback(move |e| {
let val: u32 = e.value().parse().unwrap();
write_mem(base_addr + HEALTHOFFSET, val, vec![0x40, 0x28, 0x3A0, 0x70, 0x98]);
write_mem(base_addr + HEALTHOFFSET, val, vec![0x40, 0x28, 0x3A0, 0x70, 0x90]);
});
inf_hp.set_callback({
let HASINFHP = HASINFHP.clone();
move |e| {
if e.is_toggled() {
HASINFHP.store(true, Ordering::Relaxed);
e.toggle(true);
e.set_label("On");
} else {
HASINFHP.store(false, Ordering::Relaxed);
e.toggle(false);
e.set_label("Off");
}
}
});
// Add functions that consistently get executed during event loop. This one is used
// to read out the health value and provide users with realtime feedback about their health
app::add_idle(move || {
let val = read_mem(base_addr + HEALTHOFFSET, vec![0x40, 0x28, 0x3A0, 0x70, 0x90]);
let new = format!("Set HEALTH (Current: {})", val);
health_frame.set_label(&new);
});
app::add_idle(move || {
if HASINFHP.load(Ordering::Relaxed) {
write_mem(base_addr + HEALTHOFFSET, 9999, vec![0x40, 0x28, 0x3A0, 0x70, 0x98]);
write_mem(base_addr + HEALTHOFFSET, 9999, vec![0x40, 0x28, 0x3A0, 0x70, 0x90]);
}
});
}
We can repeat the above process for the remaining values in the cheat table and thus generate the below application.
With this, we successfully accomplished the previously set goals. We wrote a small dll injector that we used to inject a small dll into the game. This spawned a simple gui application allowing us to hack the game.
You can find the full code here: darksouls3_cheats/.