O TEMA DO FÓRUM ESTÁ EM MANUTENÇÃO. FEEDBACKS AQUI: ACESSAR

Ideia pra evitar trapaça em um jogo em desenvolvimento

Iniciado por Crixus, 26/07/2020 às 20:55

26/07/2020 às 20:55 Última edição: 27/07/2020 às 22:28 por Crixus
Eu vi que existem muitas maneiras de localizar itens dentro da memória de um jogo em tempo real pra adicionar dinheiro ou munição apenas alterando o valor, mas existem jogos que isto é impossível no jogo sem modificações.

Com isso eu percebi que jogos que permitem uso de Console pra poder modificar ou softwares que peguem as informações em tempo real seriam vulneráveis, assim como a maioria do jogos simples ou antigos feitos pra Android, navegador com HTML5 ou Javascript puro.

Então eu pensei em uma maneira, mas coloquei em pratica hoje, tornar o dinheiro do jogo uma array que através de uma função criada pro jogo pra adicionar e outra pra remover através de um loop.

No exemplo que eu criei usando como base Javascript eu criei dois itens, gold e diamond me baseando na maioria dos joguinhos de Android, Gold e Diamond, ambas funções afetam ambos itens.

Veja funcionando:
http://jsfiddle.net/DevWithCoffee/xunz7gm1/4/

Função - Adicionar item:
function moneyAdd(target, amount) {
    /*
    * Syntaxe:
    *    moneyAdd(Variavel alvo criada no jogo, Valor à ser adicionado);
    * Exemplo:
    *    moneyAdd(money, 1500);
    */
    var d, nYYYY, nMM, nDD, nHH, nMi, nSS, k;
    for (i = 0; i < amount; i++) {
        d = new Date();
        nYYYY = d.getFullYear();

        nMM = d.getMonth();
        nMM = nMM + 1;
        if (nMM < 10) {
            nMM = "0" + nMM;
        }

        nDD = d.getDate();
        if (nDD < 10) {
            nDD = "0" + nDD;
        }

        nHH = d.getHours();
        if (nHH < 10) {
            nHH = "0" + nHH;
        }

        nMi = d.getMinutes();
        if (nMi < 10) {
            nMi = "0" + nMi;
        }

        nSS = d.getSeconds();
        if (nSS < 10) {
            nSS = "0" + nSS;
        }

        k = i + "" + nYYYY + "" + nMM + "" + nDD + "" + nHH + "" + nMi + "" + nSS;
        target.push(k);
    }
}


Função - Remover item:
function moneyDrop(target, amount){
    /*
    * Sintaxe:
    *    moneyDrop(Variável alvo criada no jogo, Valor à ser subtraído);
    * Exemplo:
    *    moneyDrop(money, 1500);
    */
    for(i=0;i<amount;i++){
        target.shift();
    }
}


Então seguindo as variáveis de exemplo que eu criei, para torna-las Arrays já faço isso ao declarar cada uma, de preferência no inicio do jogo:
var gold=[];
var diamond=[];


Para mostrar o valor ou comparar (Quando for fazer uma compra durante o jogo) use a propriedade length, veja o exemplo se eu quiser retornar a quantidade de "golds" no console que o jogador tem no momento:
console.log(gold.length)


Agora se eu quiser permitir uma compra de um item de 120 golds:
if(gold.length < 120){
    alert(Você não possuí ouro suficiente);
}else{
    moneyDrop(gold, 120); //retira 120 do jogador
}


Aqui o que deixei foi a ideia, dessa mesma base também estou criando aquele sistema de Loot Box, mas que ao invés de ser uma Array seria um Vetor, ou seja, os itens recebidos seriam randômicos na hora que recebesse, mas se o jogador não abrir eles não mudariam mesmo com o passar do tempo.
Mas por que teria que ser diferente do dinheiro do jogo? Digamos que o jogador não possa receber certos itens com um nível muito baixo ou que nunca tenha visto ou que não seja da região onde conseguiu o Loot Box, então se ele abrir a primeira caixa que recebeu somente após muitos niveis ainda receberia itens simples dela pois foi pega no começo do jogo.

A ideia pode ser aplicada em outras linguagens de programação, como C# que é a mais usada em jogos criados com Unity.

Não parece muito seguro, nem eficiente.

No lado da segurança, você ainda consegue facilmente ir no console e digitar "moneyAdd(gold, 999999)", ou "gold = new Array(99999)", e voilá, dinheiro infinito. Não tem muito o que dizer, é simples assim. Você pode colocar as coisas numa IIFE e/ou ofuscar o código, mas isso não impede alguém mal intencionado de pegar o código da página, deduzir seu funcionamento, alterar apenas as partes que interessam e manter o resto intocado, de forma que a funcionalidade do seu site/jogo continua igual, exceto pelo que o invasor queira alterar (no caso, talvez o balanço total da carteira?).

No lado da eficiência, sugiro dar uma olhada no coneceito de complexidade assintótica. Seu algoritmo de adicionar uma quantia em dinheiro na carteira é Θ(n) em tempo, e pra guardar o a quantidade ele gasta Θ(n) espaço, onde "n" é o valor sendo adicionado. Significa que pra adicionar 10000 unidades monetárias na conta, você executa 10000 operações (vezes um fator constante), e ocupa espaço em memória proporcional. Pra piorar, você decidiu armazenar uma string pra cada moeda, então ocupa um espaço absurdo pra valores maiores (i.e. o fator constante de uso de memória é bem alto). Outro fator que entra pra conta aqui é que você gera a string usando um objeto de data (porque não usar um objeto de data direto????), usando vários condicionais e concatenação de string, que tendem a não ser as coisas mais eficientes no ramo (i.e. o fator constante de tempo também é bem alto).

Fica o questionamento: a "segurança" que essa abordagem providencia realmente compensa o custo em eficiência? Às vezes, é o caso, ver blockchain; não acredito que seja verdade aqui. Me explico:

De modo geral, parece que você tentou seguir no caminho da segurança por obscurantismo, ou seja, complicar o processo para torná-lo difícil de entender, com a esperança de que isso afaste possíveis abusos. Como você pode ver na página da wikipedia, essa ideia já é rejeitada desde 1851. Sempre é possível, com tempo, esforço e experiência apropriados, usar engenharia reversa pra lidar com esse tipo de "segurança".

A forma que isso normalmente é feito em software é nunca confiar em nenhuma informação do lado do cliente. Simples assim. A aplicação do cliente é só uma casca visual que mostra as informações que o servidor providencia.
Absolutamente nada do que vem de uma máquina que você não controla por completo (e.g. a máquina de qualquer um acessando seu site/jogo) é confiável, portanto, quando esse tipo de confiabilidade é crítica, todo o processamento importante deve acontecer do lado de um servidor, longe dos olhos do usuário. Tem bastante complexidade envolvida e vários detalhes a serem observados e estudados, e não dá pra resumir tudo num post. Mas se seu interesse realmente é fazer um sistema online com algum tipo de segurança, recomendo bastante que dê uma olhada em conceitos do gênero.

Por exemplo, num sistema de lootbox, você isolaria a lógica de sorteio no servidor, e a única coisa que o cliente pode fazer é requisitar um sorteio. A carteira, o inventário, tudo fica no servidor, fora do alcance do cliente.
Um exemplo tosco com pseudocódigo:

Servidor
let usuários = {
  "usuárioX": { // Um usuário qualquer, com 100 granas na carteira e nenhum item
    "dinheiro": 100,
    "inventário": [],
    "hash": "k5YVNN9jMcfP2y+j4c2D1yN6Cdh4qoY9X/KG8ZQdosc=" // Hash seguro de senha, ver SHA e associados
  }
}

let items = [
  "itemA",
  "itemB",
  "itemC"
]

Controller:
   GET /token (user, hash):
       if password_verify(usuários[user].hash, hash):
           return 200, gen_token(user) // Gera um token pro usuário, leia sobre JWT, por exemplo
       else:
           return 401, 'Invalid credentials'
 
   POST /sorteio (token):
      if !verify_token(token):
          return 401, 'Invalid token'
      
      usuário = decode_token(token)

      if usuários[usuário].dinheiro < 10:
        return 400, 'Saldo insuficiente'

      sorteado = get_random(items)
      usuários[usuário].inventário.push(sorteado)
      usuários[usuário].dinheiro -= 10
      return 200, sorteado

   GET /dinheiro(token):
       if !verify_token(token):
          return 401, 'Invalid token'
      
      usuário = decode_token(token)
      return 200, usuários[usuário].dinheiro


Cliente
botão "Login":
  senha = input()
  hash = hash_password(senha)
  response = request(servidor, GET, '/token', hash)
  if response.status == 200:
    global token = response.body
  else:
    alert "Falha no login, verifique suas credenciais e tente novamente"

botão "Sortear":
  response = request(servidor, POST, '/sorteio', token)
  if response.status == 200:
    alert "Você sorteou " + response.body
    response = request(servidor, GET, '/dinheiro', token)
    if response.status == 200:
      alert "Agora você tem $" + response.body
    else:
      alert "Houve um erro com a sua requisição"
  else:
   alert "Houve um erro com a sua requisição"


Veja que não tem lógica nenhuma no cliente, só chamadas HTTP para pedir informações para o servidor. Do lado do servidor, toda requisição é validada também, sem depender de nenhuma informação dada pelo usuário exceto as da requisição. Claro, o código é bem simplista, geralmente tem vários outros passos (e os dados não ficam direto no código, ficam num banco de dados), mas já dá a ideia geral da coisa. Sugiro estudar tudo isso mais a fundo.
Veja também que podemos evitar complicações desnecessárias com isso, como usar um array para um valor que naturalmente é um número. Isso também torna tudo mais eficiente, e todo mundo sai feliz.

Já para jogos single player, onde não faz sentido ter um servidor para o jogo, não tem pra onde fugir, sempre dá para burlar as barreiras de alguma forma (porque de novo, a máquina é minha, eu controlo tudo que é executado nela, então posso alterar a memória do processo como preferir).
~ Masked

27/07/2020 às 01:07 #2 Última edição: 27/07/2020 às 22:28 por Crixus
Então, a ideia foi baseada em jogos simples que não tem sistema online, a única parte online seria ranqueamento por pontuação ou backup do progresso, mas os que vi não podiam ter o dinheiro alterado apenas mudando uma única variavel rastreável por um leitor de memória, foi este o foco da ideia, tentar reproduzir o efeito.

O uso do FOR pode ser limitado ou substituído por um SetTimeOut, ainda o jogo poderia ter um sistema que impedisse um valor muito alto, eu testei com valor acima de 750mil e tive que subir bem mais pra começar à travar, sendo este o limite dos golds que a maioria dos jogos simples permitem receber em uma única vez, sendo o computador que eu usei pros testes de um processador Intel Inside e 2GB de RAM.
Obs: Eu ainda poderia limitar à 1000 obrigando o programado executar a mesma função varias vezes pra inteirar o valor desejado se este for muito alto o que daria mais facilidade pra complementar com um sistema que checasse o formato de cada moeda e deletasse as invalidas ainda por cima.

A ideia é impedir que softwares de emuladores de jogos de Android modifiquem a memória do jogo por um APP (destes padrões pra trapaça), as funções podem ser encriptadas assim como as variáveis de valores monetários do jogo, o fundamento é tornar a engenharia reversa cansativa pro trapaceiro e não impedir realmente de o fazer, sacou? Cansar o jogador trapaceiro é eficiente sim, o que por sua vez cada camada torna mais seguro, por exemplo se cada item da array for encriptada em Base64, só pra cansar mais um pouco.

Sobre o espaço de armazenamento, pode parecer grande, mas usando um formato único pra cada moeda adquirida não será muito maior do que informações de arquivos JSON usados como banco de dados ou Slots de Saves, ocuparia mais espaço se fosse um objeto Json ao invés de Array, lembrando que estás informações vão para a memória do navegador/webView quando um jogo inicia.

A finalidade do tópico é mostrar como funciona o conceito e não como esconde-lo, o que poderá ser providenciado depois, mas eu precisei mostrar as funções pra entender como dispara-las.

Sobre dificultar pro trapaceiro (Edit 1):
Lembra-se do sistema EasyRpg que permite emular um jogo de Rm2k no navegador sem RTP ou qualquer outro recurso via Server?
https://gamejolt.com/games/pacman_aw/408267
https://bossrpg.itch.io/pacman-alternative-worlds

Então, ele roda tudo no cliente, eu tentei de todas as maneiras editar o código fonte, a camada do código é bem profunda, apesar de eu me considerar leigo no assunto já fiz isso antes.
Eu não consegui ter acesso ao código pra tentar modificar ou ver como eles conseguiram interpretar um jogo de Rm2k aparentemente somente com Javascript.

Edit 2:
Ah, lembrando que se você for abrir o arquivo principal do EasyRPG vai notar um tipo de encriptação, acho que era HEXDecimal, mas se conseguir fazer a engenharia reversa seria bem legal, eu gostaria de saber como é o código que permitiu emular um jogo de Rm2k no navegador.

Pra ilustrar meu ponto, consegui achar a variável na memória só navegando, usando as ferramentas de desenvolvedor do navegador:


(Tem um opção "Store as global variable" também, que me permite manipular esse objeto como eu quiser depois)


Foi bem fácil achar a variável usando as ferramentas de gerenciamento de memória do Chrome, também ^
O procedimento foi começar a monitorar alterações de memória e clicar no botão algumas vezes pra ver onde a alteração ocorria (cada barrinha ali é um clique meu no botão). Eu já sabia a implementação, mas dá pra chutar com bastante confiança que as strings ali são o que representa o dinheiro, porque eu cliquei no botão +100 3 vezes e tem ~300 delas.

É um procedimento bem simples e bem comum em engenharia reversa, provavelmente seria a primeira opção de alguém mais instruído que quisesse burlar seu sistema.

Outro ponto notável é, sim, o uso de memória. 2kb para armazenar um número é absurdo (se fosse um int, daria pra armazenar de 0 a 2^2000 = 114813069527425452423283320117768198402231770208869520047764273682576626139237031385665948631650626991844596463898746277344711896086305533142593135616665318539129989145312280000688779148240044871428926990063486244781615463646388363947317026040466353970904996558162398808944629605623311649536164221970332681344168908984458505602379484807914058900934776500429002716706625830522008132236281291761267883317206598995396418127021779858404042159853183251540889433902091920554957783589672039160081957216630582755380425583726015528348786419432054508915275783882625175435528800822842770817965453762184851149029376 (não é um valor aleatório, é 2^2000 mesmo, pode checar; calculei usando Ruby).
Isso sem contar a memória que você gasta criando um Date a cada iteração do loop, que só vai ser liberada depois pelo garbage collector (que provavelmente vai sofrer, também).

Entendo seu ponto que tem coisas mais pesadas tipo o banco de dados ou mesmo um único arquivo de imagem, mas a questão é que pra esses dados você absolutamente precisa de toda a informação neles (se você começar a apagar dados do arquivo de imagem vai perder píxels, ora), enquanto que no seu sistema de dinheiro a informação dentro do array é irrelevante. A única coisa que importa mesmo é o tamanho dela (que é um número inteiro!!), e poderia ser trocado por um valor numérico sem muita perda de segurança, mas um ganho significativo em performance.

Sobre trocar o loop por um setTimeout: não resolveria o problema de performance, só procrastina o cálculo pro futuro haha

Resumindo:

  • Trocar um número por array é ineficiente em termos de espaço e tempo, e colocar valores de data no array além de não servir a nenhum propósito em particular (você não faz nenhuma validação, podia até ser null) só piora esse aspecto do algoritmo;
  • A troca também não dificulta a manipulação dos dados de nenhuma forma significativa, em 15 segundos alguém com um pouco de experiência em engenharia reversa consegue encontrar a variável responsável por armazenar o dinheiro e manipular ela sem muita dificuldade.

O EasyRPG é mais complicado de mexer porque ele usa WebAssembly pra rodar os jogos (não Javascript), então tem uma camada de virtualização em cima que torna o trabalho de fuçar nas coisas mais difícil.

E hexadecimal não é algoritmo criptográfico, é uma base numérica (ou radical). Base64, mesma coisa (como está no nome, é só um número com radical 64).
Ambos são geralmente usados pra representar dados binários em meios onde isso seria inviável ou inconveniente, como no navegador ou requisições HTML, por exemplo. Nesse caso, provavelmente é o binário do executável do jogo mesmo, que o EasyRPG dá um jeito de rodar usando WebAssembly.




Dito tudo isso, vou propor um algoritmo mais eficiente e mais seguro:

function money(element) {
  let value = 0;
  let checkSeed = new Int32Array(16);
  let checkSum = new Int32Array(16);

  // Valores encapsulados, retorna só métodos de acesso.
  // Assim os valores não ficam associados em um único objeto.
  return {
    element,
    getValue: () => value,
    setValue: v => value = v,
    getSeed: i => checkSeed[i],
    genSeed: () => crypto.getRandomValues(checkSeed),
    getCheck: i => checkSum[i],
    setCheck: (i, v) => checkSum[i] = v,
  }
}

let gold = money(document.getElementById("gold"));
setMoney(gold, 0);

function validateMoney(ref) {
  for (let i = 0; i < 16; i++) {
    let seed = ref.getCheck(i) ^ ~(ref.getValue() << i);
    if (seed != ref.getSeed(i)) {
      setMoney(ref, 0); // Se o valor foi alterado manualmente, volta pra 0
      break;
    }
  }
}

function getMoney(ref) {
  validateMoney(ref);
  return ref.getValue();
}

function setMoney(ref, value) {
  ref.genSeed();
  ref.setValue(value);
  for (let i = 0; i < 16; i++) {
    ref.setCheck(i, ref.getSeed(i) ^ ~(value << i));
  }
  ref.element.innerText = value;
}

function addMoney(ref, n) {
  setMoney(ref, getMoney(ref) + n);
}


Funcionando: http://jsfiddle.net/n8s93uc6/26/

O tempo e espaço gastos com isso é constante, e não tão absurdo (são 33 Int32, basicamente, dá uns 132 bytes). Além disso, tem um mecanismo de checagem pra impedir alterações a menos que o atacante conheça o algoritmo, o que dificulta bastante a vida, se você se der o trabalho de ofuscar e esconder tudo direitinho.
~ Masked

Masked definiu o maior problema de segurança atual: trate qualquer entrada do cliente como potencialmente maliciosa. E não basta apenas manter o processamento sensível no servidor, mas ter métodos de validá-lo em tempo real. Digamos que todas as tentativas de burlar o algorítimo local falhem, um atacante iria tentar enganar o servidor manipulando as requisições.

Configurando uma proxy no navegador é possível interceptar todas as requisições que ele envia para o servidor com o qual está interagindo. Programas como o Burp Suite e Charles Proxy facilitam bastante o processo. Uma vez em funcionamento, o jogo pode seguir normalmente. Só é preciso que, no momento em que o cliente informa a pontuação final ao servidor, a que será colocada no ranking, ele altere o valor pro número desejado.

Se não houver nenhum mecanismo de validação, já era. Se houver, ainda vai dar um trabalho pro atacante ver como a aplicação responde a requisições falsas e legítimas, mas é possível inferir como o(s) mecanismo(s) funciona(m) e manipular uma que passe pelos filtros. 100% de segurança não existe, então você precisa garantir que o resultado não valha o esforço.  :shrug:

27/07/2020 às 21:12 #5 Última edição: 27/07/2020 às 22:46 por Crixus
@Brandt:
- Realmente quem tem mais experiência levaria algum tempo efetuando os testes, porém ele teria de jogar e custaria tempo pra ter certeza, em dispositivos moveis ele não teria tal facilidade, essa é a ideia.
- O número do 2^2000 é bem longo mesmo, poderia colocar dentro de um [*CODE][*/CODE], concordo, realmente é um consumo bem grande de memória, não imaginei que gastaria tanta memória, já que a variaveis são as mesmas.
- Por que usei data? Foi uma mera base que uso a tempos, como invocar um recurso do servidor (JSON,XML,TXT) que não mantenha CACHE, então usei o mesmo principio de maneira que o número não repetisse e futuramente pudesse ser usado pra validar os itens em base da data recorrente.
- Sobre o WebAssembly, obrigado por me esclarecer, ninguém até hoje soube me explicar.
- Esse seu código de exemplo realmente ficou bem interessante e provavelmente mais confuso pra que ainda está engatinhando na programação, pra ser franco estou tentando entender o por que de cada função.
- E por último e não menos importante, eu aprendi o que sei lendo, explorando, etc, como a maioria, por falta de tempo e outras coisas pessoais não consegui me aprofundar, me diga, você aprendeu tanto estudando em casa ou tem algum curso com módulos que leve ao nível avançado? Realmente estou admirado.

@CORVO:
- Sobre o Proxy, sim, realmente é possível, mas existem jogos que o ranking é retornado do servidor e se ele considerar um valor fora do comum em um curto tempo na validação seria uma das maneiras de impedir que houvesse trapaça, pois imagino que é mais difícil simular o servidor do que o cliente, ou estou enganado?
- A ideia em geral mesmo foi mostrar a ideia.

CitarRealmente quem tem mais experiência levaria algum tempo efetuando os testes, porém ele teria de jogar e custaria tempo pra ter certeza, em dispositivos moveis ele não teria tal facilidade, essa é a ideia.

Assim, meu ponto é que a dificuldade de achar e manipular o array é mais ou menos a mesma de achar e manipular um número, por isso não tem muita vantagem. Se é difícil achar o array no mobile, vai ser difícil achar o número também, aí não compensa o esforço.


CitarO número do 2^2000 é bem longo mesmo, poderia colocar dentro de um [*CODE][*/CODE], concordo, realmente é um consumo bem grande de memória, não imaginei que gastaria tanta memória, já que a variaveis são as mesmas.

Deixei fora da tag pra dar efeito dramático mesmo, o scroll bugado contribui com isso haha
Outro detalhe, o consumo de memória é grande, e aumenta linearmente conforme o valor na carteira. Isso era eu com 300 moedas, se eu tivesse 10x mais moedas, o array ocuparia 10x mais espaço. Isso não acontece com números inteiros, que no geral são sempre armazenados em 4 bytes (32-bits).


CitarPor que usei data? Foi uma mera base que uso a tempos, como invocar um recurso do servidor (JSON,XML,TXT) que não mantenha CACHE, então usei o mesmo principio de maneira que o número não repetisse e futuramente pudesse ser usado pra validar os itens em base da data recorrente.

O problema disso é que o Date do JS usa a data da máquina do cliente, não de um servidor. Então assim como todo o resto não dá pra confiar nele, e é relativamente fácil de alterar, basta rodar um scriptzinho sobrescrevendo a função Date antes do jogo.


CitarSobre o WebAssembly, obrigado por me esclarecer, ninguém até hoje soube me explicar.

Estamos aí \o
É mais obscuro mesmo, mas dá pra fazer umas coisas bem impressionantes.


CitarEsse seu código de exemplo realmente ficou bem interessante e provavelmente mais confuso pra que ainda está engatinhando na programação, pra ser franco estou tentando entender o por que de cada função.

É bem confuso mesmo, essa é a ideia kkk

Posso dar uma explicação breve:

Código: javascript
function money(element) {
  let value = 0;
  let checkSeed = new Int32Array(16);
  let checkSum = new Int32Array(16);

  // Valores encapsulados, retorna só métodos de acesso.
  // Assim os valores não ficam associados em um único objeto.
  return {
    element,
    getValue: () => value,
    setValue: v => value = v,
    getSeed: i => checkSeed[i],
    genSeed: () => crypto.getRandomValues(checkSeed),
    getCheck: i => checkSum[i],
    setCheck: (i, v) => checkSum[i] = v,
  }
}


Isso é um construtor. É um pouco diferente do "new XPTO" que você vê por aí, porque eu não quis vincular as propriedades de valor, checkSum e seed num objeto (que seria fácil de encontrar na memória), então usei arrow functions pra capturar os objetos. Esse é o único jeito de ter variáveis privadas de verdade em Javascript atualmente, aliás.
As funções são:
- getValue e setValue: getter e setter básico pro valor; é necessário aqui porque o valor é privado.
- getSeed(i): retorna o valor de uma posição do seed aleatório (tem 16, de 0-15)
- genSeed: gera um seed aleatório, substituindo o atual; isso é útil pra trocar o seed sempre que o dinheiro muda, pra dar um nível a mais de aleatoriedade no algoritmo
- getCheck(i) e setCheck(i): verificam e alteram a soma de checagem, servem pra uso interno de verificação

Tem também uma propriedade "element", que é útil na hora de mudar o valor na tela. Não afeta muito a segurança, apesar de deixar um caminho pro atacante até o objeto. É um possível ponto de melhoria, não quis me preocupar muito com isso.

Código: javascript
function validateMoney(ref) {
  for (let i = 0; i < 16; i++) {
    let seed = ref.getCheck(i) ^ ~(ref.getValue() << i);
    if (seed != ref.getSeed(i)) {
      setMoney(ref, 0); // Se o valor foi alterado manualmente, volta pra 0
      break;
    }
  }
}


Esse é o algoritmo que verifica se o dinheiro não foi alterado manualmente. A soma de checagem é feita com operações binárias nos números, porque são fáceis de reverter para verificar depois.
Se a verificação falha ele volta o dinheiro pra 0 como penalidade.

function getMoney(ref) {
  validateMoney(ref);
  return ref.getValue();
}

function setMoney(ref, value) {
  ref.genSeed();
  ref.setValue(value);
  for (let i = 0; i < 16; i++) {
    ref.setCheck(i, ref.getSeed(i) ^ ~(value << i));
  }
  ref.element.innerText = value;
}

function addMoney(ref, n) {
  setMoney(ref, getMoney(ref) + n);
}


O getMoney é bem direto, o que o setMoney faz é definir o valor do dinheiro e gerar uma nova soma de checagem, usando o novo valor e um seed que ele gera também. Um detalhe importante é que essa função tem toda a lógica que "garante" que o dinheiro é válido, então ela não pode ser exposta de jeito nenhum, todo o foco de ofuscar e dificultar o acesso deve estar focado nessa função (e de preferência na de validação também).

Espero ter esclarecido.


CitarE por último e não menos importante, eu aprendi o que sei lendo, explorando, etc, como a maioria, por falta de tempo e outras coisas pessoais não consegui me aprofundar, me diga, você aprendeu tanto estudando em casa ou tem algum curso com módulos que leve ao nível avançado? Realmente estou admirado.

Principalmente lendo e explorando também, acho que também dei sorte de ter começado cedo e com bastante tempo livre e já feito colégio técnico e seguido isso como carreira. Acho que não tem um livro ou artigo que você vá ler e que vai de repente abrir seus olhos para um mundo novo de programação e magia assim não, pra ser sincero haha
O jeito é ir fuçando e sempre que possível tentar aprender com os outros, de vez em quando assistindo uma aula sobre um ou outro assunto. Recomendo ver o canal do MIT Open Course Ware, tem muita aula muito boa sobre computação lá.
~ Masked