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 called CreateCustomMission and main 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.