The retail version of DayZ, versions prior to 1.24, had a script API function that was abusable allowing persistent script execution on the client without triggering anti-cheat measures. The exploit can be considered Living Off the Land as the game exposed this API without the need of any memory manipulation of the game’s process or its services.
Preface
DayZ loads the majority of its game logic in script form, which can be modded legitimately by modders. Mods can be loaded on both the client and server, which is validated by signatures of the archive format, PBO. DayZ loads these scripts from mods and the game itself through a compiler and are organized into a ScriptModule
. There are 5 total vanilla script modules, 1_Core
, 2_GameLib
, 3_Game
, 4_World
, and 5_Mission
. These script modules are compiled sequentially from 1-5 at the start of the game, and are labeled based on their responsibilities. When creating mods, modders will add and adapt scripts that exist in the above modules only.
Custom Script Modules
Within DayZ’s vanilla scripts, the following class is available within the 1_Core
script module:
//! Module containing compiled scripts.
class ScriptModule
{
private void ~ScriptModule();
/*!dynamic call of function
when inst == NULL, it's global function call, otherwise it's method of class
returns true, when success
The call creates new thread, so it's legal to use sleep/wait
*/
proto volatile int Call(Class inst, string function, void parm);
/*!dynamic call of function
when inst == NULL, it's global function call, otherwise it's method of class
returns true, when success
The call do not create new thread!!!!
*/
proto volatile int CallFunction(Class inst, string function, out void returnVal, void parm);
proto volatile int CallFunctionParams(Class inst, string function, out void returnVal, Class parms);
proto native void Release();
/**
\brief Do load script and create ScriptModule for it
\param parentModule Module
\param scriptFile Script path
\param listing ??
\returns \p ScriptModule Loaded scripted module
@code
???
@endcode
*/
static proto native ScriptModule LoadScript(ScriptModule parentModule, string scriptFile, bool listing);
}
The function of interest that was exploited is called LoadScript
. This function is responsible with creating a brand new ScriptModule
outside of the existing vanilla script modules. This can easily be paired with CallFunction
and CallFunctionParams
to access functions within any class reference, including custom script modules.
Example:
// set our parent script module as the highest accessible script module, 5_Mission
// load script file from '../Documents/DayZ/entry.c'
ScriptModule module = ScriptModule.LoadScript(GetGame().GetMission().MissionScript, "$saves:entry.c", true);
if (module)
{
// call a global accessible function within the custom script module
// pass down our current reference to the custom script module
module.CallFunction(null, "InitEntry", null, module);
}
Contents of entry.c:
void InitEntry(ScriptModule currentModule)
{
Print("Hello from custom script module: " + currentModule);
currentModule.Release(); // unloads and destroys the script module reference
}
Within this new custom script module we can create new classes, layouts, and access nearly any function and/or field from other class references. This script module can NOT use the modded
keyword on classes that exist in other script modules, as those have already been compiled and cannot be changed. The modded
keyword allows you to override functions and constants within the class you’re modding, which is the full potential of scripting.
Exploitation
The legitimate way of loading custom scripts is to load them into a mod, but this will not allow us to join the majority of servers, as they verify signatures of PBOs loaded. But there is one other way to load scripts onto the client without loading a mod, which is passing a path to the -mission
parameter upon launch. Missions have an entry point of their own, the following is required to have a proper mission:
- Directory name needs to be missionName.mapName:
example.chernarusplus
- An entry script called
init.c
within the mission directory init.c
should consist of two functions calledCreateCustomMission
andmain
as well a class that inherits from a mission
example.chernarusplus/init.c
// custom mission that inherits from the main menu mission
class ExampleMission : MissionMainMenu
{
void ExampleMission()
{
Print("Hello from ExampleMission ctor");
}
}
// called from C++; path is the value from mission parameter
Mission CreateCustomMission(string path)
{
return new ExampleMission();
}
void main();
Upon loading, we can check out script log and see that we’ve written SCRIPT: Hello from ExampleMission ctor
into our script log, which means we have execution of scripting without needing to load any mod. Now we can apply loading a custom script module along with our mission.
class AbuseMission : MissionMainMenu
{
// store our mission into '../Documents/DayZ/abuse.chernarusplus/'
const string ENTRY_POINT = "$saves:abuse.chernarusplus/Scripts/AbuseLoader.c";
// once the mission have started, load our custom script module
override void OnMissionStart()
{
super.OnMissionStart();
if (!FileExist(ENTRY_POINT))
{
ShowDialog("AbuseLoader.c not found!");
return;
}
// create our custom script module with 5_Mission as parent
ScriptModule module = ScriptModule.LoadScript(GetGame().GetMission().MissionScript, ENTRY_POINT, true);
if (!module)
{
// if the is a script syntax error, notify
// check script log to find which line is responsible
ShowDialog("Unable to compile AbuseLoader.c");
return;
}
// call global function in `AbuseLoader.c` and pass our custom module down
module.CallFunction(null, "AbuseLoaderEntry", null, module);
}
// visual prompt in game to allow us to easily be notified
static void ShowDialog(string message)
{
GetGame().GetUIManager().ShowDialog("Abuse Loader", message, 1, DBT_OK, DBB_OK, DMT_EXCLAMATION, GetGame().GetUIManager().GetMenu());
}
}
Mission CreateCustomMission(string path)
{
return new AmbushMission();
}
void main();
Now we’ve successfully loaded a custom script module, we can join any server from the in-game server browser and our custom script module will not be released or destroyed. Any creative and knowledgeable scripter could easily create their own cheats without the fear of being BattlEye banned, such as a visual ESP.
Hot Reload
If we keep the idea of responsibility in mind, we can achieve hot reloading of our custom scripts by load two custom script modules, one for the loader and one for our custom logic. The following is an example of achieving hot reloading by using our top most custom script module AbuseLoader
to reload our child script module via key press.
// store global constants of paths to our known resources
const string ABUSE_ROOT = "$saves:abuse.chernarusplus/";
const string ABUSE_SCRIPTS = ABUSE_ROOT + "Scripts/";
const string ABUSE_LAYOUTS = ABUSE_ROOT + "Layouts/";
const string ABUSE_ENTRY = ABUSE_SCRIPTS + "Abuse.c";
void AbuseLoaderEntry(ScriptModule module)
{
new AbuseLoader(module);
}
class AbuseLoader
{
protected ScriptModule AbuseLoaderScript;
protected ScriptModule AbuseScript;
static ref AbuseLoader Instance;
void AbuseLoader(ScriptModule module)
{
Instance = this;
AbuseLoaderScript = module;
// subscribe our method from call queue upon ctor
GetGame().GetUpdateQueue(CALL_CATEGORY_SYSTEM).Insert(OnFrame);
Initialize();
}
void ~AbuseLoader()
{
// unsubscribe our method from call queue upon dtor
GetGame().GetUpdateQueue(CALL_CATEGORY_SYSTEM).Remove(OnFrame);
}
void Initialize(bool reload = false)
{
if (AbuseScript)
{
// run our unload logic, allowing us to safely clean up our references and changes
AbuseScript.CallFunction(null, "UnloadAbuse", null, null);
AbuseScript.Release(); // destroy and unload our abuse script module
}
// load our abuse script module with the loader script module as parent
AbuseScript = ScriptModule.LoadScript(AbuseLoaderScript, ABUSE_ENTRY, true);
if (!AbuseScript)
{
GetGame().GetUIManager().ShowDialog("Abuse Loader", "Unable to load Abuse script module!", 1, DBT_OK, DBB_OK, DMT_EXCLAMATION, GetGame().GetUIManager().GetMenu());
return;
}
// initialize our abuse scripts and pass if it's a reload or not
AbuseScript.CallFunction(null, "InitAbuse", null, reload);
}
void OnFrame()
{
// handle key presses via user action input system
HandleInput(GetGame().GetInput());
}
void HandleInput(notnull Input input)
{
// up arrow
if (input.LocalPress("UAVoiceDistanceUp", false))
{
// reload abuse scripts
AbuseLoader.Instance.Initialize(true);
}
}
}
Conclusion
Bohemia Interactive was notified by me about this exploitable API in 1.23, and was resolved rather quickly within the next major release, 1.24. Bohemia Interactive patched this exploitable API only ScriptModule.LoadScript
on the client retail version of the game, it’s still accessible in the diagnostic version of the game as well as the retail server. It is unknown if any other sources abused this API. With this exploited API, a research opportunity was taken to develop a list of other client authoritative exploits, with reproducibility. These exploits were also disclosed to Bohemia Interactive, which are still pending to be resolved, but they include some very well known results that have affected several players and server owners.