Skip to content
Permalink
Browse files
Teach SAMPCAC how to do detection runs
  • Loading branch information
RussellLVP committed Jul 21, 2020
1 parent 3ae58c7 commit 751c2564d110eed670d6a1e60e54377735b0b3de
Showing 9 changed files with 265 additions and 3 deletions.
@@ -119,6 +119,7 @@ export class Player extends Supplementable {
#serial_ = null;
#ipAddress_ = null;
#isNpc_ = null;
#version_ = null;

#selectObjectResolver_ = null;

@@ -144,6 +145,7 @@ export class Player extends Supplementable {
this.#serial_ = murmur3hash(this.#gpci_ || 'npc');
this.#ipAddress_ = pawnInvoke('GetPlayerIp', 'iS', this.#id_);
this.#isNpc_ = !!pawnInvoke('IsPlayerNPC', 'i', this.#id_);
this.#version_ = this.#isNpc_ ? 'NPC' : pawnInvoke('GetPlayerVersion', 'iS', this.#id_);
}

notifyDisconnecting() { this.#connectionState_ = Player.kConnectionClosing; }
@@ -172,6 +174,8 @@ export class Player extends Supplementable {

get serial() { return this.#serial_; }

get version() { return this.#version_; }

get packetLossPercentage() {
return toFloat(pawnInvoke('NetStats_PacketLossPercent', 'i', this.#id_))
}
@@ -23,6 +23,7 @@ export class MockPlayer extends Player {
#ping_ = 30;
#ipAddress_ = null;
#isNpc_ = null;
#version_ = null;

#isServerAdmin_ = false;

@@ -80,6 +81,7 @@ export class MockPlayer extends Player {
this.#serial_ = murmur3hash(this.#gpci_ || 'npc');
this.#ipAddress_ = params.ip || '127.0.0.1';
this.#isNpc_ = params.npc || false;
this.#version_ = params.version || '0.3.7-mock';

this.#lastDialogPromiseResolve_ = null;
this.#lastDialogPromise_ = new Promise(resolve => {
@@ -100,6 +102,8 @@ export class MockPlayer extends Player {

get serial() { return this.#serial_; }

get version() { return this.#version_; }

get packetLossPercentage() { return this.#packetLossPercentage_; }
set packetLossPercentageForTesting(value) { this.#packetLossPercentage_ = value; }

@@ -0,0 +1,80 @@
// Copyright 2020 Las Venturas Playground. All rights reserved.
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

// Represents a SAMPCAC detector as defined in our private JSON file. Detectors can either be based
// on expected readings, or on unexpected reasons. Immutable after construction.
export class Detector {
// Type of supported detectors.
static kTypeAllowList;
static kTypeBlockList;

#address_ = null;
#bytes_ = null;
#name_ = null;
#type_ = null;

#resultBytes_ = null;
#resultChecksum_ = null;

constructor(detector) {
if (!detector.hasOwnProperty('name') || typeof detector.name !== 'string')
throw new Error(`Each detector must have a human readable name.`);

this.#name_ = detector.name;

if (!detector.hasOwnProperty('address') || typeof detector.address !== 'number')
throw new Error(`${this}: Non-numeric address specified in configuration.`);

if (!detector.hasOwnProperty('bytes') || typeof detector.bytes !== 'number')
throw new Error(`${this}: Non-numeric byte length specified in configuration.`);

this.#address_ = detector.address;
this.#bytes_ = detector.bytes;

if (detector.hasOwnProperty('blocked')) {
this.#type_ = Detector.kTypeBlockList;

this.#resultBytes_ = detector.blocked.bytes ?? null;
this.#resultChecksum_ = detector.blocked.checksum ?? null;

} else if (detector.hasOwnProperty('expected')) {
this.#type_ = Detector.kTypeAllowList;

this.#resultBytes_ = detector.expected.bytes ?? null;
this.#resultChecksum_ = detector.expected.checksum ?? null;

} else {
throw new Error(`${this}: Detector either has to be a blocked or allowed type.`);
}

if (this.#resultBytes_ !== null && !Array.isArray(this.#resultBytes_))
throw new Error(`${this}: Result bytes must be specified as an array.`);

if (this.#resultChecksum_ !== null && typeof this.#resultChecksum_ !== 'number')
throw new Error(`${this}: Result checksum must be specified as a number.`);

if (this.#resultBytes_ === null && this.#resultChecksum_ === null)
throw new Error(`${this}: Either the result bytes or checksum must be specified.`);
}

// Gets the name for this detector, will be shown in the dialogs.
get name() { return this.#name_; }

// Gets the address at which memory has to be read.
get address() { return this.#address_; }

// Gets the number of bytes that have to be read from the given address.
get bytes() { return this.#bytes_; }

// Gets the expected result of the Detector. The polarity might have to be negated depending
// on the |type| of detector this instance represents.
get resultBytes() { return this.#resultBytes_; }
get resultChecksum() { return this.#resultChecksum_; }

// Gets the type of detector this instance represents.
get type() { return this.#type_; }

// Returns a textual representation of this detector.
toString() { return `[object Detector("${this.#name_}")]`; }
}
@@ -2,22 +2,124 @@
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

import { Detector } from 'features/sampcac/detector.js';
import { DetectorResults } from 'features/sampcac/detector_results.js';

import { equals } from 'base/equals.js';

// File (not checked in) in which detectors are located. Not required.
const kDetectorConfiguration = 'detectors.json';

// Time after which we consider a memory read to have timed out.
export const kMemoryReadTimeoutMs = 3500;

// Manages the SAMPCAC detectors that are available on the server. Will load its information from
// a configuration file, unless tests are active, in which case they can be injected.
export class DetectorManager {
detectors_ = null;
natives_ = null;
responseResolvers_ = null;

constructor(natives) {
this.detectors_ = null;
this.natives_ = natives;
this.responseResolvers_ = new Map();

server.playerManager.addObserver(this);
}

// Initializes the detectors from scratch. Will be lazily called the first time a detection run
// is started for a particular player, or the detectors are being reloaded by management.
initializeDetectors() {
this.detectors_ = new Set();

let configuration = null;
try {
configuration = JSON.parse(readFile(kDetectorConfiguration));
} catch {
return; // bail out, the file does not exist
}

if (!Array.isArray(configuration))
throw new Error(`Expected the detector configuration to be an array.`);

for (const detectorConfiguration of configuration)
this.detectors_.add(new Detector(detectorConfiguration));
}

// ---------------------------------------------------------------------------------------------

// Runs the necessary checks on the given |player|, and returns an instance of DetectorResults
// to communicate back the |player|'s state. Could take multiple seconds.
async detect(player) {
if (this.detectors_ === null)
this.initializeDetectors();

const results = new DetectorResults();

// (1) Populate the meta-data fields of the results.
results.version = player.version;

if (this.natives_.getStatus(player)) {
results.sampcacVersion = this.natives_.getClientVersion(player).join('.');
results.sampcacHardwareId = this.natives_.getHardwareID(player);
}

results.minimized = player.isMinimized();

// (2) Run each of the detectors for the |player| in parallel and populate the results in
// the |results.detectors| map. There is no expectation for that map to be sorted.
results.detectors = new Map();

const tasks = [];

for (const detector of this.detectors_) {
tasks.push(this.requestDetection(player, detector).then(result => {
results.detectors.set(detector.name, result);
}));
}

// Wait for all the |tasks| to have been completed.
await Promise.all(tasks);

// (3) Return the |results| to the caller who requested this detection run.
return results;
}

// Requests the |detector| to run for the given |player|. Will return the result of the request
// as one of the DetectorResult.kResult* constants.
async requestDetection(player, detector) {
const response = await this.requestMemoryRead(player, detector.address, detector.bytes);
if (response === null)
return DetectorResults.kResultUnavailable;

// (1) Determine whether the |response| is a checksum (true) or an array of bytes (false).
const isChecksum = typeof response === 'number';

// (2) Consider the |response| based on the type of |detector| we're dealing with.
switch (detector.type) {
case Detector.kTypeAllowList:
if (isChecksum && detector.resultChecksum === response)
return DetectorResults.kResultClean;

if (!isChecksum && equals(detector.resultBytes, response))
return DetectorResults.kResultClean;

break;

case Detector.kTypeBlockList:
if (isChecksum && detector.resultChecksum === response)
return DetectorResults.kResultDetected;

if (!isChecksum && equals(detector.resultBytes, response))
return DetectorResults.kResultDetected;

break;
}

return DetectorResults.kResultUndeterminable;
}

// ---------------------------------------------------------------------------------------------

// Requests a memory read from the |player| at the given |address| (in GTA_SA.exe address space)
@@ -53,8 +155,8 @@ export class DetectorManager {

// Request a checksum for GTA_SA.exe's .text section as the contents are well known, so we
// don't need the granularity and can reduce the data being transfered.
address < 0x856A00 ? this.natives_.readMemoryChecksum(player, address, bytes)
: this.natives_.readMemory(player, address, bytes);
this.natives_.readMemory(player, address, bytes);
this.natives_.readMemoryChecksum(player, address, bytes)

const result = await promise;

@@ -2,6 +2,7 @@
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

import { DetectorResults } from 'features/sampcac/detector_results.js';
import { MockSAMPCACStatus } from 'features/sampcac/mock_sampcac_natives.js';

import { kMemoryReadTimeoutMs } from 'features/sampcac/detector_manager.js';
@@ -22,6 +23,23 @@ describe('DetectorManager', (it, beforeEach) => {
feature.natives_.setStatusForTesting(gunther, status);
})

it('is able to run a detection against a particular player', async (assert) => {
const resultPromise = manager.detect(gunther);

// Fast-forward since we can't mock all the memory addresses.
await server.clock.advance(kMemoryReadTimeoutMs);

const result = await resultPromise;

assert.instanceOf(result, DetectorResults);
assert.equal(result.version, '0.3.7-mock');
assert.typeOf(result.sampcacVersion, 'string');
assert.typeOf(result.sampcacHardwareId, 'string');
assert.isFalse(result.minimized);

assert.instanceOf(result.detectors, Map);
});

it('is able to request memory reads from players', async (assert) => {
status.writeMemoryChecksum(0x43A4B0, 1337);
status.writeMemory(0x867198, [ 0x61, 0x66, 0x72, 0x6F ]); // afro
@@ -0,0 +1,38 @@
// Copyright 2020 Las Venturas Playground. All rights reserved.
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

// Encapsulates the results of a SAMPCAC detection run. Self-contained to enable other parts of the
// code to rely on this functionality without concern.
export class DetectorResults {
static kResultUnavailable = 0; // results could not be obtained from the player
static kResultUndeterminable = 1; // results could not be interpreted against the detector
static kResultClean = 2; // results came back negative
static kResultDetected = 3; // results came back positive

// ---------------------------------------------------------------------------------------------
// Section: Meta-information about the player
// ---------------------------------------------------------------------------------------------

// Version of the SA-MP client that they're using.
version = null;

// Version of the SAMPCAC client that they're using. May be NULL.
sampcacVersion = null;

// Hardware ID assigned to the player by SAMPCAC. Based on the VMProtect Hardware ID algorithm,
// and is thus easily gameable. (https://helloacm.com/decode-hardware-id/)
sampcacHardwareId = null;

// Boolean indicating whether the player is currently minimized. This influences whether results
// will be made available, as their client has to respond to it.
minimized = null;

// ---------------------------------------------------------------------------------------------
// Section: Detectors
// ---------------------------------------------------------------------------------------------

// Map of <detector name, detector result> for each of the defined detectors. Is not guaranteed
// to have entries, as the detectors are not open sourced. In effectively randomized order.
detectors = null;
}
@@ -49,7 +49,7 @@ export class EventMonitor extends SAMPCACEventObserver {
// Called when the memory at the given |address| has been read in their GTA_SA.exe memory space,
// with the actual memory contents being written to |buffer| as an Uint8Buffer.
onPlayerMemoryRead(player, address, buffer) {
this.manager_.onMemoryResponse(player, address, buffer);
this.manager_.onMemoryResponse(player, address, [ ...buffer ]);
}

// Called when the |player| has taken a screenshot.
@@ -33,6 +33,16 @@ export default class SAMPCAC extends Feature {
this.monitor_ = new EventMonitor(this.manager_);
}

// ---------------------------------------------------------------------------------------------
// Public API of the SAMPCAC feature
// ---------------------------------------------------------------------------------------------

// Runs the necessary checks on the given |player|, and returns an instance of DetectorResults
// to communicate back the |player|'s state. Could take multiple seconds.
async detect(player) { return await this.manager_.detect(player); }

// ---------------------------------------------------------------------------------------------

dispose() {
this.monitor_.dispose();
this.monitor_ = null;
@@ -10,12 +10,18 @@ export class SAMPCACNatives {
static kGameOptionManualReloading = 1;
static kGameOptionDriveOnWater = 2;
static kGameOptionFireproof = 3;
static kGameOptionSprint = 4;
static kGameOptionInfiniteSprint = 5;
static kGameOptionInfiniteOxygen = 6;
static kGameOptionInfiniteAmmo = 7;
static kGameOptionNightVision = 8;
static kGameOptionThermalVision = 9;

// Options that can be passed to the kGameOptionSprint value.
static kSprintDefault = 0;
static kSprintAllSurfaces = 1;
static kSprintDisabled = 2;

// Glitches that can be toggled with SAMPCAC.
static kGlitchQuickReload = 0;
static kGlitchFastFire = 1;

0 comments on commit 751c256

Please sign in to comment.