mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-04 20:57:49 +00:00
281 lines
9.2 KiB
C++
281 lines
9.2 KiB
C++
#include "stdafx.h"
|
|
#include "NetPmc.h"
|
|
|
|
// for teaDecrypt
|
|
#include "security/FuzzyTokens.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <algorithm>
|
|
|
|
#include "Replicator.h"
|
|
#include "ServerReplicator.h"
|
|
#include "ClientReplicator.h"
|
|
#include "v8datamodel/DataModel.h"
|
|
#include "util/rbxrandom.h"
|
|
|
|
// Design notes from 4 December 2015:
|
|
// This is the Networked Program Memory Checker.
|
|
// In the first implementation, there was no easy way to distribute information to the
|
|
// RCC server. However, the post-build release patcher did work.
|
|
//
|
|
// The flow works like this:
|
|
// (build)
|
|
// The patcher will generate a group of 128 different challenges -- different starting/ending/seed values
|
|
// These will be generated before the main PMC is run, as it will modify the .rdata section.
|
|
// These will be encrypted using salsa20. This can be generated on the server.
|
|
// The results from each challenge will be generated and stored as well, but will be encrypted using tea.
|
|
//
|
|
// (server)
|
|
// The server will locally regenerate the decryption key sequence
|
|
// The server will send a selector + key to the game client
|
|
// This will be added to the "challengesInFlight"
|
|
//
|
|
// (client)
|
|
// The client will decrypt the info for this challenge
|
|
// The hash will be performed
|
|
// The client will send the selector, its response, and the encrypted correct response to the server.
|
|
//
|
|
// (server)
|
|
// The server will decrypt the correct response and compare it to the given response
|
|
// The inFlight list will be scanned and the entry removed.
|
|
//
|
|
// if the inFlight list will have a limit on how large it can grow.
|
|
//
|
|
// This security mechanism will also add some checks to avoid running in places where it is likely to
|
|
// be debugged.
|
|
//
|
|
// The choice of tea/salsa was based on the existence of code online.
|
|
DYNAMIC_FASTINTVARIABLE(HashConfigP3, 4) // players
|
|
DYNAMIC_FASTINTVARIABLE(HashConfigP4, 1000) // parts
|
|
DYNAMIC_FASTINTVARIABLE(HashConfigP5, 1) // down
|
|
DYNAMIC_FASTINTVARIABLE(HashConfigP6, 1) // up
|
|
DYNAMIC_FASTINTVARIABLE(HashConfigP8, 64) // pending limit
|
|
|
|
namespace RBX {
|
|
namespace Security{
|
|
std::vector<uint32_t> hackFlagVector;
|
|
const volatile uint32_t kHackFlagVectorDecrypt = 1; // might not be random
|
|
|
|
|
|
#ifndef RBX_STUDIO_BUILD
|
|
const volatile NetPmcChallenge kChallenges[kNumChallenges] = {};
|
|
|
|
uint32_t netPmcHashCheck(const NetPmcChallenge& challenge)
|
|
{
|
|
const uint32_t kMult2 = 0xBADA55;
|
|
const uint32_t kMult1 = 0xC0FFEE11;
|
|
unsigned int v1 = challenge.seed;
|
|
unsigned int v2 = challenge.seed;
|
|
unsigned int v3 = challenge.seed;
|
|
unsigned int v4 = challenge.seed;
|
|
const uintptr_t* p = reinterpret_cast<const uintptr_t*>(challenge.base);
|
|
|
|
for (size_t i = 0; i < challenge.size/16; ++i)
|
|
{
|
|
v1 += *(p+0) * kMult2;
|
|
v2 += *(p+1) * kMult2;
|
|
v3 += *(p+2) * kMult2;
|
|
v4 += *(p+3) * kMult2;
|
|
v1 = _rotl(v1, 17);
|
|
v2 = _rotl(v2, 17);
|
|
v3 = _rotl(v3, 17);
|
|
v4 = _rotl(v4, 17);
|
|
v1 *= kMult1;
|
|
v2 *= kMult1;
|
|
v3 *= kMult1;
|
|
v4 *= kMult1;
|
|
p+=4;
|
|
}
|
|
return v1^v2+v3-v4;
|
|
}
|
|
#endif
|
|
|
|
#ifdef RBX_RCC_SECURITY
|
|
std::vector<NetPmcChallenge> netPmcKeys = generateNetPmcKeys();
|
|
|
|
NetPmcServer::NetPmcServer() : // some are set to safe, but odd defaults
|
|
isGameCreator(true)
|
|
, userHasActivity(false)
|
|
, gameHasManyParts(false)
|
|
, gameHasManyPlayers(false)
|
|
, challengeIdx(0)
|
|
, challengesRecv(0)
|
|
, challengesSent(0)
|
|
{
|
|
challenges.resize(kNumChallenges);
|
|
for (size_t i = 0; i < kNumChallenges; ++i)
|
|
{
|
|
challenges[i] = i;
|
|
}
|
|
std::random_shuffle(challenges.begin(), challenges.end());
|
|
}
|
|
|
|
// the intent it to make it much harder to detect this mechanism by making it
|
|
// only work in places that are actually games.
|
|
bool NetPmcServer::canSendChallenge(const RBX::Network::Replicator* rep) /*non-const*/
|
|
{
|
|
auto dm = DataModel::get(rep);
|
|
if (dm)
|
|
{
|
|
auto remotePlayer = rep->getRemotePlayer();
|
|
if (remotePlayer)
|
|
{
|
|
isGameCreator = (dm->getCreatorID() == remotePlayer->getUserID());
|
|
if (!isGameCreator && !gameHasManyPlayers)
|
|
{
|
|
// recheck to see if there are enough players
|
|
gameHasManyPlayers = (dm->getNumPlayers() > DFInt::HashConfigP3);
|
|
}
|
|
if (!isGameCreator && !gameHasManyParts)
|
|
{
|
|
// recheck to see if there are enough parts
|
|
gameHasManyParts = (dm->getNumPartInstances() > DFInt::HashConfigP4);
|
|
}
|
|
}
|
|
}
|
|
userHasActivity = (rep->stats().kiloBytesReceivedPerSecond >= DFInt::HashConfigP5) && (rep->stats().kiloBytesSentPerSecond >= DFInt::HashConfigP6);
|
|
return (!isGameCreator && gameHasManyPlayers && gameHasManyParts && userHasActivity);
|
|
}
|
|
|
|
unsigned int NetPmcServer::generateDebugInfo(const RBX::Network::Replicator* rep, uint32_t& sent, uint32_t& recv, uint32_t& pending) const
|
|
{
|
|
unsigned int debugInfo = 0;
|
|
|
|
debugInfo |= (isGameCreator ? (1<<0) : 0);
|
|
debugInfo |= (gameHasManyPlayers ? (1<<1) : 0);
|
|
debugInfo |= (gameHasManyParts ? (1<<2) : 0);
|
|
debugInfo |= ((rep->stats().kiloBytesReceivedPerSecond >= DFInt::HashConfigP5) ? (1<<3) : 0);
|
|
debugInfo |= ((rep->stats().kiloBytesSentPerSecond >= DFInt::HashConfigP6) ? (1<<4) : 0);
|
|
|
|
debugInfo |= ((challengesRecv > 1) ? (1<<5) : 0);
|
|
debugInfo |= ((challengesSent > 1) ? (1<<6) : 0);
|
|
|
|
debugInfo |= ((challengesRecv > 32) ? (1<<7) : 0);
|
|
debugInfo |= ((challengesSent > 32) ? (1<<8) : 0);
|
|
|
|
debugInfo |= ((challengesRecv > 128) ? (1<<9) : 0);
|
|
debugInfo |= ((challengesSent > 128) ? (1<<10) : 0);
|
|
|
|
sent = challengesSent;
|
|
recv = challengesRecv;
|
|
pending = challengesInFlight.size();
|
|
|
|
return debugInfo;
|
|
}
|
|
|
|
bool NetPmcServer::tooManyPending() const
|
|
{
|
|
return (challengesInFlight.size() > static_cast<unsigned int>(DFInt::HashConfigP8));
|
|
}
|
|
|
|
uint8_t NetPmcServer::getRandomChallenge()
|
|
{
|
|
if (challengeIdx == challenges.size()-1)
|
|
{
|
|
std::random_shuffle(challenges.begin(), challenges.end());
|
|
challengeIdx = 0;
|
|
}
|
|
return challenges[challengeIdx++];
|
|
}
|
|
|
|
bool NetPmcServer::sendChallenge(uint8_t idx)
|
|
{
|
|
challengesSent++;
|
|
boost::mutex::scoped_lock lock(flightMutex);
|
|
challengesInFlight.push_back(idx);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NetPmcServer::removeFromList(uint8_t challengeNum)
|
|
{
|
|
challengesRecv++;
|
|
boost::mutex::scoped_lock lock(flightMutex);
|
|
auto loc = std::find(challengesInFlight.begin(), challengesInFlight.end(), challengeNum);
|
|
if (loc != challengesInFlight.end())
|
|
{
|
|
challengesInFlight.erase(loc);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool NetPmcServer::checkResult(uint8_t idx, uint32_t response, uint64_t correct) const
|
|
{
|
|
uint64_t correctDecrypted = RBX::Security::teaDecrypt(correct);
|
|
if (response != (correctDecrypted&0xFFFFFFFF))
|
|
{
|
|
// hash mismatch
|
|
return false;
|
|
}
|
|
if (correctDecrypted>>32 != idx)
|
|
{
|
|
// attempted replay attack
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
// From github: https://github.com/andres-erbsen/salsa20/blob/master/salsa20.c
|
|
|
|
// rotate x to left by n bits, the bits that go over the left edge reappear on the right
|
|
#define R(x,n) (((x) << (n)) | ((x) >> (32-(n))))
|
|
|
|
// addition wraps modulo 2^32
|
|
// the choice of 7,9,13,18 "doesn't seem very important" (spec)
|
|
#define quarter(a,b,c,d) do {\
|
|
b ^= R(d+a, 7);\
|
|
c ^= R(a+b, 9);\
|
|
d ^= R(b+c, 13);\
|
|
a ^= R(c+d, 18);\
|
|
} while (0)
|
|
|
|
void salsa20_words(uint32_t *out, uint32_t in[16]) {
|
|
uint32_t x[4][4];
|
|
int i;
|
|
for (i=0; i<16; ++i) x[i/4][i%4] = in[i];
|
|
for (i=0; i<10; ++i) { // 10 double rounds = 20 rounds
|
|
// column round: quarter round on each column; start at ith element and wrap
|
|
quarter(x[0][0], x[1][0], x[2][0], x[3][0]);
|
|
quarter(x[1][1], x[2][1], x[3][1], x[0][1]);
|
|
quarter(x[2][2], x[3][2], x[0][2], x[1][2]);
|
|
quarter(x[3][3], x[0][3], x[1][3], x[2][3]);
|
|
// row round: quarter round on each row; start at ith element and wrap around
|
|
quarter(x[0][0], x[0][1], x[0][2], x[0][3]);
|
|
quarter(x[1][1], x[1][2], x[1][3], x[1][0]);
|
|
quarter(x[2][2], x[2][3], x[2][0], x[2][1]);
|
|
quarter(x[3][3], x[3][0], x[3][1], x[3][2]);
|
|
}
|
|
for (i=0; i<16; ++i) out[i] = x[i/4][i%4] + in[i];
|
|
}
|
|
|
|
// inputting a key, message nonce, keystream index and constants to that transormation
|
|
void salsa20_block(uint8_t *out, uint8_t key[32], uint64_t nonce, uint64_t index) {
|
|
static const char c[16] = "expand 32byte k"; // arbitrary constant
|
|
#define LE(p) ( _byteswap_ulong(*reinterpret_cast<const unsigned long*>(p)) )
|
|
uint32_t in[16] = {LE(c), LE(key), LE(key+4), LE(key+8),
|
|
LE(key+12), LE(c+4), nonce&0xffffffff, nonce>>32,
|
|
index&0xffffffff, index>>32, LE(c+8), LE(key+16),
|
|
LE(key+20), LE(key+24), LE(key+28), LE(c+12)};
|
|
#undef LE
|
|
uint32_t wordout[16];
|
|
salsa20_words(wordout, in);
|
|
int i;
|
|
for (i=0; i<64; ++i) out[i] = 0xff & (wordout[i/4] >> (8*(i%4)));
|
|
}
|
|
|
|
// enc/dec: xor a message with transformations of key, a per-message nonce and block index
|
|
void salsa20(uint8_t *message, uint64_t mlen, uint8_t key[32], uint64_t nonce) {
|
|
int i;
|
|
uint8_t block[64];
|
|
for (i=0; i<mlen; i++) {
|
|
if (i%64 == 0) salsa20_block(block, key, nonce, i/64);
|
|
message[i] ^= block[i%64];
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|