Arma: Créer des callbacks sécurisés

L’objectif de cet article est d’expliquer comment exécuter du code sur des machines distantes. Cet article décrit rétrospectivement comment les développeurs faisaient avant que BIS implémente officiellement des fonctionnalités tel que: BIS_fnc_MP puis remoteExec dans son moteur.

Cet article permet de mieux comprendre comment fonctionne les échanges réseaux avec Arma, et sur quelle base de réflexions le netcode actuel d’ARMA a été construit. Tout ce qui est décrit dans les lignes ci-dessous continue de fonctionner, et correspond à peu de choses prêt à ce qu’a été BME (bus message exchange) sous ARMA2, décrit plus tard par Killzone Kid sous ARMA3, puis finalement intégré en script par BIS.

BIS a depuis implémenté une méthode native remoteExec plus performante. Il existe également maintenant une version BME 2.0 qui apporte des fonctionnalités supplémentaires.

Je te recommande avant de poursuivre, de lire l’article concernant les variables qui donne des bases solides 🙂 Sache également que j’ai modifié cet article initialement rédigé par Killzone Kid pour le réactualiser, corriger ou modifier certains points avec lesquels je n’étais pas en accord. Tu pourras donc retrouver si cela t’intéresse sur son site site l’article original.

Bien, commençons maintenant …

Notre objectif est d’implémenter un mécanisme classique et standard qui répond au cahier des charges suivant. Il existe d’autre cas de figure, mais ici on ne parlera que de celui-ci.

Cahier des charges:

  • objectif: déclencher une fonction à distance sur toutes les machines
  • pas d’échanges de code via le réseau
  • les fonctions appelées sont déclarées en tant que variable globale sur toutes les machines
  • seul les données transitent sur le réseau en tant que paramètre de la fonction à appeler

Créer une fonction sur toutes les machines est facile. Quand la mission démarre, le fichier init.sqf se charge et s’exécute sur chacune des machines, y compris sur le serveur. Définir une fonction dans ce fichier, ou dans tout fichier qui serait appelé à partir de ce fichier fera l’affaire.

Déclencher l’exécution de cette fonction depuis n’importe quelle machine, est également facile . C’est une combinaison d’une commande publicVariable qui broadcaste une variable (event) et d’handlers qui la réceptionnent de l’autre coté, et qui déclenchent une fonction de callback.

Tout ce que tu as à faire est donc de mettre en place cette fonction de callback, qui se comporte comme un routeur applicatif et qui appellera ta fonction cible.

Pour utiliser la fonction callback, il faut donc lui passer en paramètre le nom de la fonction en STRING. Ce nom de fonction devra être ensuite être traduit dynamiquement en code pour être finalement exécuté.

Au tout début, la méthode pour réaliser ce type d’opération dynamique était d’utiliser une combinaison de commande : call compile format.

our_callback = {
    private ["fnc","_params"];
    _fnc = _this select 0;
    _params = _this select 1;
    call compile format ["%2 call fnc_%1", _fnc, _params];
};

Outre le fait que cela rendait la lecture et la compréhension du code particulièrement compliqué. Ce type de commande a aussi un impact en terme de performance et de sécurité. Pour identifier le type de fonction (distante) et sécuriser un minimum l’échange en différenciant une commande netcode d’une commande local, on préfixait la commande réseau avec « fnc_ ».

Définissons maintenant la fonction que nous allons appeler sur toutes les machines par exemple dans le fichier init.sqf:

fnc_lolfunc = {
    private "_params";
    _params = _this select 0;
    hint str _params;
};

Pour que cela fonctionne, nous avons également besoin de déclarer un event handler qui dès qu’il recevra un event du réseau, déclenchera la fonction callback, qui elle même appellera la fonction fnc_lolfunc.

our_publicvariable = []; 
"our_publicvariable" addPublicVariableEventHandler {
    (_this select 1) call our_callback;
};

Testons !

Pour que notre event handler se déclenche, il faut définir et broadcaster notre variable publique à partir d’une autre machine.

our_publicvariable = ["lolfunc", ["LMAO!"]];
publicVariable "our_publicvariable";

Après, l’exécution de notre callback fonction sur les machines distances, nous obtiendrons l’équivalent de ce code.

call {["LMAO!"] call fnc_lolfunc};

Tout semble fonctionner mais cela pose un problème de sécurité, démonstration par l’exemple:

our_publicvariable = ["lolfunc;...malicious code...", ["LMAO!"]];
publicVariable "our_publicvariable";

Le résultat exécuté sur les machines distantes sera:

call {["LMAO!"] call fnc_lolfunc;...malicious code...};

Comme tu peux le voir, la fonction de callback va d’abord exécuter la fonction comme prévu, et ensuite exécuter le code malicieux.

Une des méthodes pour corriger ce problème de sécurité a donc été de remplacer l’usage du call compile format par l’utilisation des namespace.

_params call missionNamespace getVariable [format ["%1", _fnc], fallback_function];

On réduit les problèmes de performance, le risque d’injection de code, et on y adjoint également une fonction fallback qui permet d’identifier les appels de code distant anormaux.

Nous devons également vérifier que nous ne pouvons pas appeler de notre callback, notre callback ce qui générait une boucle infinie.

our_log = {
    diag_log (format [
        "ERROR: Call to non-existent function '%1' (Passed params: %2)",
        _this select 0, 
        _this
    ])
};
our_callback = {
    private ["_fnc","_code"];
    _fnc = _this select 0;
    if !(_fnc isEqualTo "our_callback") then {
        _code = missionNamespace getVariable [format ["%1", _fnc], our_log];
        if ((_this select 2) isEqualTo "call") then {
            _this call _code;
        } else {
            _this spawn _code;
        };
    };
};

Comme précédemment, on définie la fonction et un event handler

fnc_lolfunc = {
    hint (_this select 1);
};

"our_publicvariable" addPublicVariableEventHandler {
    (_this select 1) call our_callback;
};

et on déclenche le handler via le broadcast de notre « our_publicvariable ».

our_publicvariable = ["fnc_lolfunc", "LMAO!", "spawn"];
publicVariable "our_publicvariable";

Une autre chose à savoir, est que la commande publicVariable ne déclenche pas le event handler de la machine sur laquelle elle est exécutée. Tu dois donc faire appel directement à ta fonction sur cette machine.

A ce titre BME (bus message exchange) gère ce principe de localité et permet d’avoir un comportement cohérente identique en local et MP.

Si tout cela a fonctionne comme prévu, il faut maintenant protéger le code de l’écrasement, en utilisant la commande compileFinal. Pour cela, je t’invite à lire l’article sur compilefinale, si tu ne l’as déjà fais.

Pour augmenter le niveau de sécurité, il est également possible d’utiliser une white liste des fonctions autorisées à être appelée via la fonction callback.

our_whitelist = {
    _this in [
        "fnc_lolfunc",
        "function2",
        "function3"
    ]
};

our_log = {
    diag_log (format [
        "ERROR: Call to non-existent function '%1' (Passed params: %2)",
        _this select 0, 
        _this
    ])
};

our_callback = {
    private ["_fnc","_code"];
    _fnc = _this select 0;
    if (_fnc call our_whitelist) then {
        _code = missionNamespace getVariable [format ["%1", _fnc], our_log];
        if ((_this select 2) == "call") then {
            _this call _code;
        } else {
            null = _this spawn _code;
        }
    }
};

Sache néanmoins, que ces méthodes ont leur propre limite. Killezone Kid présumait que les failles de sécurité sont liées simplement au code SQF alors que beaucoup d’entre elles sont liées à l’implémentation même du moteur, ou aux interactions entre le moteur et des outils extérieurs.

Les cheaters utilisent des outils de debug qui permettent de desofusquer le code, d’éditer à volée le contenu des variables, d’envoyer du code au client, de faire des appels distants sans même toucher à vos pbo / scripts.

Il faut donc bien prendre ce point en compte, certes il faut coder propre (dixit le getvariable dans cet article), mais aujourd’hui la sécurité sur ARMA repose principalement sur battleye qui exclue les joueurs qui n’utilisent pas du contenu signé et vérifié par BIS (notamment les dlls).

Votre commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l’aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google

Vous commentez à l’aide de votre compte Google. Déconnexion /  Changer )

Image Twitter

Vous commentez à l’aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l’aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s