🤔 Para Refletir :
"A pressa é a inimiga do grinding."
- Moge

Idéia pra evitar trapaça em um jogo em desenvolvimento

DevWithCoffee

Cidadão
Membro
Membro
Juntou-se
02 de Abril de 2019
Postagens
134
Bravecoins
216
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:
Game - Basic Gold and Diamond - JSFiddle - Code Playground

Função - Adicionar item:
Código:
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);
  }
}

function moneyDrop(target, amount) {
  /*
   * Syntaxe:
   *    moneyDrop(Variavel alvo criada no jogo, Valor à ser subtraido);
   * Exemplo:
   *    moneyDrop(gold, 1500);
   */
  for (i = 0; i < amount; i++) {
    target.shift()
  }
}

Função - Remover item:
Código:
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:
Código:
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:
Código:
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.
 
Última edição:
Legal o exemplo que você criou mas vale lembrar que esses jogos de Android com "gold" e "diamond" são jogos online e jogos online tem um servidor por trás pra cuidar de autenticidade de dados.

O que eu quero dizer é que em um jogo single-player/CO-OP não vale a pena gastar performance pra "proteger" o jogo.
Jogos como Skyrim adicionam o console no jogo e deixam em produção porque alterar o seu dinheiro não vai afetar o jogo de ninguém além do seu.

Já jogos competitivos online (Dark Souls, Crossfire etc.) conseguem se certificar de que nada ta errado do lado do servidor, sem atrapalhar a performance do jogo do lado do cliente.
Quando alguem tenta alterar a memória no client e fazer uma compra, por exemplo, o servidor vai comparar o valor da compra, o valor de dinheiro no servidor e dizer se é possível ou não comprar. No caso, o dinheiro do client é apenas a ilustração do que tem no servidor.

E, pra fechar, mesmo que você faça dessa forma ainda é completamente possível trapacear nesse sistema.
Literalmente, se eu fizer um gold.push(...(new Array(500))); eu adiciono 500 de gold nesse seu sistema. A unica coisa que mudou é que ao invés de alterar uma integer eu adicionei itens numa array.

TL;DR:
Não tem porquê essas medidas de "segurança" num jogo singleplayer/coop. E em jogos competitivos online você tem o servidor pra poder fazer isso sem afetar a performance do jogo (de loop em loop, a complexidade só aumenta).
 
A ideia foi baseada em jogos simples que não tem sistema de PVP e que podem ser jogados offline, o uso da internet é apenas para ranqueamento por pontuação ou backup do save, mas os que vi não podiam ter o dinheiro alterado apenas mudando o valor, foi este o foco da ideia.
... jogos simples ou antigos feitos pra Android ...

Então eu acho que tem sim razão pra usar tais medidas de segurança, eu mesmo alterei variáveis ou dados armazenados no navegador apenas digitando o valor do dinheiro em jogos, a ideia é criar mais uma camada complexa pra que visualmente o jogador trapaceiro tenha mais dificuldade de inserir valores.

A sua maneira funciona, porém as Arrays tem um formato numérico único, o qual permitiria usar um verificador logo em seguida, ainda não criado, o que impediria de usar números repetidos ou fora do formato, tudo pra dificultar e não impedir a trapaça.

(Eu respeito um bom trapaceiro que consegue achar brechas em um código, não foi a toa que fui banido da Condado na época do Casino)
 
Última edição:
Achei bem legal o sistema. Por isso é sempre muito importante sempre encapsular propriedades.
Faz um tempinho que não mexo com JS, mas eu fiquei brincando de tentar trapacear e realmente funcionou! hauhuahsuhaus !!
 
Obrigado Dr., a ideia ainda pode ser implementada, mas eu postei isso por que pensei a poucos dias.

O padrão do formato que usei foi [Fila do Item][Ano][Mês][Dia][Hora][Minuto][Segundo] pra evitar que outro item repetido apareça, mas provavelmente irei modificar o formato e comprimir em Base64, o que eu já uso pra outros sistemas WEB.
 
Obrigado Dr., a ideia ainda pode ser implementada, mas eu postei isso por que pensei a poucos dias.

O padrão do formato que usei foi [Fila do Item][Ano][Mês][Dia][Hora][Minuto][Segundo] pra evitar que outro item repetido apareça, mas provavelmente irei modificar o formato e comprimir em Base64, o que eu já uso pra outros sistemas WEB.
Isso, encriptar os elementos vai reforçar a segurança. Sobre o formato, não me lembro se no JS tem o mesmo lance que tem no Java, onde vc consegue recuperar os milissegundos corridos desde 01/01/1970 ao instanciar um novo Date, que aí já enxutava o código do elemento antes de meter o Base64 nele 😁
 
Tem sim, MyVar.getTime() ou Date.now(), acredito que seja isto que usem pra calcular diferença entre datas e prazos.
 
Última edição:
Prezado, boa tarde!
Inicialmente, gostaria de destacar que a ideia é magnífica. Acho o ponto muito interessante para evitar que os trapaceiros consigam rastrear o código exato que permita modificar os recursos do jogo, e achei a forma bastante criativa (mesmo não entendendo quase nada de programação). São ideias como essa que me enchem de júbilos.​
Confesso que as ponderações da @Kaw são pertinentes ao assunto também. É claro que sempre haverá um jeito para tentar burlar o sistema, por isso a ideia de dificultar é ainda mais interessante.​
Ademais, achei muito interessante a segunda ideia:​
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.
Tenho bastante curiosidade quanto a esse tipo de recurso para criar um mecanismo que impeça o jogador de abrir o loot e dar load se não gostar do resultado. Repetindo o processo até vir algo que ele goste.​
Mantenha-nos informado do seu progresso. Acho o debate muito válido.​
Abraços.​
 
Trazendo pra cá o que disse na CRM:

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
Código:
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
Código:
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"

@Dobberman Essa seria uma solução pro problema da lootbox. Não é tão emocionante quando usar um array pra representar um número, talvez, mas é mais seguro certamente.

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).

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

NMsAfRl.png

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

uMMgpuk.png

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.

E hexadecimal e base64 não são algoritmos criptográficos, são bases numéricas (ou radicais). 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. Não servem pra esconder informação.




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

Código:
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: Game - Basic Gold and Diamond - JSFiddle - Code Playground
Adicionei um botão "Try to hack" também, simulando um procedimento que alterasse diretamente o valor na carteira.

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.

A ideia é basicamente ter uma sequência criptográfica aleatória e usar ela pra bater com o valor do dinheiro e gerar um checksum. O algoritmo de checksum que eu usei é ridículo, existem vários bem melhores (por exemplo, você poderia misturar os valores da sequência além de usar só o número), mas serve de demonstração do conceito. Note que mesmo assim quando você for mandar as informações pro servidor (pra criar um ranking, por exemplo) o usuário pode interceptar e alterar o payload, afetando a pontuação. Uma contramedida seria gerar o seed randômico no servidor, mas aí tem algumas complicações adicionais e ainda não é lá muito seguro.

Você também poderia usar algo pra mascarar o valor da variável que guarda o valor do dinheiro em si, isso tornaria mais difícil encontrar ela usando ferramentas de análise de memória. De toda forma, isso torna bem mais difícil burlar o sistema do que trocar um número por um Array, e também é ordens de magnitude mais eficiente.
 
Última edição:
@Dobberman sobre o sistema de Loot Box eu já o fiz sem usar as camadas, tornando as variáveis Arrays memorizadas visiveis, mas para o jogador não, vou fazer um tópico depois só pra isto.

@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, 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.
 
Voltar
Topo Inferior