Auditoria de exploits · AutoMarket

Zerar em 1 Minuto

Dez falhas encontradas na leitura do código. Duas encerram o jogo em menos de um minuto, três anulam o conceito de programação que o desafio existe para ensinar, o resto acelera a economia muito além do ritmo planejado. No fim, os scripts verificados que resolvem cada tipo de cliente.

A vitória pretendida

Comprar o upgrade Zerar (UpgradeData.gd:118), que exige 5 diamantes e o delivery_online comprado. Diamantes só vêm de declarações aprovadas no Delivery — uma por relatório, com 60 s de cooldown. O caminho legítimo custa R$ 1.800 em upgrades e cerca de 4 minutos de espera pura.

Vitória instantânea 2 falhas · < 1 min

Qualquer uma das duas leva do save novo à tela de conclusão sem atender um único cliente corretamente.

01. send("cheat") libera o menu de debugCrítico

TransactionManager.gd:74 · game.gd:87-119 · game.tscn:152 · debug_menu.gd

submit() intercepta o argumento antes de validar a resposta e libera o menu secreto. A string está em texto puro no repositório, e o desbloqueio é gravado no save — vale para sempre naquele slot.

Acesso rápido
  1. Com um cliente na tela — serve o do tutorial, que aparece no passo 5 — rode no console:
    int main() {
        send("cheat");
    }
  2. Abra o menu: um clique no canto superior esquerdo (hotspot invisível de 36×36 px) ou triplo clique no painel de dinheiro, com menos de 1200 ms entre cliques.
  3. Ligue Dinheiro infinito, compre os 11 upgrades da árvore, clique +1 diamante cinco vezes e compre Zerar.

Custo: um cliente perdido — o cheat força correto = false.

02. Save em JSON sem assinaturaCrítico

SaveManager.gd:74 · SaveManager.gd:316-344 · SaveManager.gd:406 · UpgradeManager.gd:184

load_save_data aplica todo upgrade listado sem checar preço nem requisito. E _looks_like_save_data aceita o arquivo se ele tiver uma única chave conhecida.

Acesso rápido
  1. Abra o save:
    ~/.local/share/godot/app_userdata/AutoMarket/save_slot_1.json
  2. Substitua o conteúdo por uma linha — isso já dispara complete_game() no load:
    {"shop": {"comprados": {"zerar": true}}}

Na versão web nem precisa de editor de texto: os botões EXPORTAR e IMPORTAR do menu principal fazem o mesmo. Variantes úteis: game.diamonds: 5 com delivery_online: true deixa o Zerar comprável de verdade, com overlay de vitória; tutorial.completed: true pula o tutorial; delivery.next_report_unix: 0 zera o cooldown.

Anulam o desafio educacional 3 falhas

Não aceleram o relógio, mas permitem chegar ao fim sem exercitar nenhum dos conceitos que o jogo se propõe a ensinar.

03. declare_profit() é bruteforçávelAlto

DeliverySystem.gd:127-156 · DeliverySystem.gd:385 · delivery_panel.gd:69

Os valores são checados antes da validação pedagógica, e _reject() não tem custo, não tem limite de tentativas e não consome o relatório.

Acesso rápido

São 216 combinações — entregas de 0 a 5 em três categorias. Um while triplo dentro de um único main() acerta sempre. Pior: o painel do Delivery já mostra os três números na tela, então uma tentativa basta.

04. A recursão obrigatória aceita uma função-iscaAlto

DeliveryProgramValidator.gd:45-67 · executor.gd:525-530

Nada liga a recursão ao valor declarado. O validador só olha a estrutura do AST — quer uma função não-main que chame a si mesma, com um if e um ramo que retorna sem recursão. O runtime só verifica se essa função apareceu duas vezes na pilha depois de get_deliveries().

Acesso rápido
  1. Declare uma função que não faz nada de útil:
    int isca(int n) {
        if (n <= 0) {
            return 0;
        }
        return isca(n - 1);
    }
  2. Chame isca(2) uma vez, depois de get_deliveries() — antes disso delivery_report_id ainda é 0 e o runtime nem observa a pilha (executor.gd:525).
  3. Calcule o lucro com a forma fechada base * (2^n - 1) ou com uma tabela de 6 entradas. Passa nos dois checks.

05. Errar é grátis e o cliente não tem paciênciaAlto

ClientSpawner.gd:150 · TransactionManager.gd:61 · cliente.gd:64

Resposta errada não custa dinheiro nem estoque — consume_items só roda se acertar — e não existe timer de paciência. Enquanto isso, os valores do pedido aparecem escritos no diálogo.

Acesso rápido

Ler os números na tela e mandar send(31.50) fixo, cliente a cliente. Todo desafio fora do Delivery é resolvível sem escrever uma linha de lógica, sem risco nenhum em errar.

Aceleram a progressão 5 falhas

Jogando dentro das regras, encurtam drasticamente as duas barreiras do jogo: o dinheiro e o tempo.

06. Recompensa de estoque fixa com bônus cumulativoMédio

ChallengeSystem.gd:33-40 · ChallengeSystem.gd:138

São 24 fixos por pedido, independente do valor pedido, enquanto os itens custam de R$ 3 a R$ 6 e o cliente consome só dois tipos.

Acesso rápido

Com premium 1-3 (×1,75), troco (+4), cliente ouro (+8, que em pedidos de estoque nem aumenta a dificuldade) e estoque cheio (×1,5): ~94 por cliente a cada 1-3 s, contra ~9 a 24 de custo. Um loop while com sensor() que atende e chama buy_stock() mantém o multiplicador 1,5× permanente. A árvore inteira se paga em poucos minutos.

07. dinheiro_minimo é código mortoMédio

UpgradeData.gd · 12 ocorrências

O campo está declarado em todo upgrade, mas nenhum arquivo do projeto o lê.

Acesso rápido

Não há nada a fazer para explorar: o ritmo de progressão planejado simplesmente não existe. Cada upgrade fica comprável assim que a cadeia requer é satisfeita — lang_if deveria surgir aos R$ 120 e está disponível aos R$ 100.

08. As palavras da linguagem não são travadasMédio

parser.gd:410-435 · builtins.gd:9-18

Só o if checa FeatureManager antes de compilar (parser.gd:411). while, for, funções e arrays funcionam desde o save novo, sem comprar nada. get_stock() e buy_stock() também não têm trava de feature nos builtins.

Acesso rápido

Os R$ 110 do Aprender while() não compram a palavra while, compram o tipo de cliente que ela resolve. Um loop já roda antes disso — só não existe ainda um pedido que precise dele.

09. Espaço pula os diálogosMédio

dialog_screen.gd:29-35

A recompensa só é creditada quando o diálogo de resultado fecha, e o próximo cliente só nasce depois disso.

Acesso rápido

Martelar Espaço. Corta até 4 s por linha (AUTO_ADVANCE_MAX_DELAY) vezes cerca de 5 linhas por cliente. Triplica o throughput sem nada além do teclado.

10. O cooldown depende do relógio do sistemaMédio

DeliverySystem.gd:224

O tempo restante sai de Time.get_unix_time_from_system() comparado com um valor persistido no save.

Acesso rápido

Adiantar o relógio do sistema operacional libera o relatório na hora. É o único freio temporal real do jogo — 4 × 60 s para juntar os 5 diamantes — e não tem defesa alguma.

Códigos que resolvem verificados contra o interpretador

Se o objetivo é só zerar, a falha 01 é mais rápida que qualquer script. O que vem abaixo é o caminho jogando dentro das regras: um script pronto por tipo de pedido, na ordem em que os upgrades liberam cada tipo.

Três regras que quebram o script antes de rodar

Leia antes de copiar qualquer coisa
  1. void não existe. O lexer monta a tabela de palavras-chave a partir dos tokens KW_* (lexer.gd:24-29), e só há int e float. Em void main(), void vira um identificador solto e o parser cobra o ponto e vírgula que viria depois dele — é exatamente o Linha 1, Col 10: Faltou 'SEMICOLON' aqui. Encontrei 'IDENTIFIER' no lugar. Toda função começa com int ou float.
  2. Parâmetro de função só pode ser int. parser.gd:356 consome KW_INT fixo na lista de parâmetros. int f(float x) não compila; variáveis locais float funcionam normalmente.
  3. if, while e for exigem chaves. O executor empilha o corpo direto como bloco (executor.gd:389, :404, :821). Um corpo sem {} não é um BlockNode, não tem .statements, e a execução quebra dentro da Godot sem mensagem no console do jogo.

O que chega em input(), o que sai em send()

ChallengeSystem.gd:129-163 · TransactionManager.gd:39-47

A fila de entradas e a quantidade de valores esperados mudam a cada upgrade comprado. Errar o formato é o motivo mais comum de um script correto perder o cliente.

A partir deFila do input()send() espera
Save novoa, bsend(total)
Aprender if()a, bsend(total) com desconto
Aprender while()p1 … pn, -1send(total)
Cliente com trocop1 … pn, -1, pagosend(total, troco)
Abrir estoquepreço, qtd, …, -1, pagosend(total, troco)
Um send() por cliente

submit() marca transaction_closing = true na primeira chamada (TransactionManager.gd:64). send(total); send(troco); em duas linhas perde o cliente: a primeira é validada com um valor a menos que o esperado e a segunda retorna false sem fazer nada. Os dois valores vão na mesma chamada — send(total, troco).

Cliente de soma save novo

Dois preços na fila, um total esperado. Sem if comprado ainda, nenhum desconto é aplicado no cálculo do jogo.

int main() {
    float a = input();
    float b = input();
    send(a + b);
}

Lista de preços com sentinela Aprender while()

De 3 a 5 preços terminados em -1. Como Aprender if() é pré-requisito, o desconto de 10% acima de R$ 50 já está valendo (ChallengeSystem.gd:26-29) e precisa entrar na conta.

int main() {
    float total = 0;
    float preco = input();

    while (preco > 0) {
        total = total + preco;
        preco = input();
    }

    if (total > 50) {
        total = total * 0.9;
    }

    send(total);
}

A condição é preco > 0, não preco != -1: com a fila esgotada next_input() passa a devolver 0 para sempre (TransactionManager.gd:42-43), e o teste contra -1 nunca sairia do loop.

Cliente com troco Cliente com troco

Mesma lista, mais um valor no fim: o que o cliente entregou. Agora são dois valores esperados, na ordem total e troco.

int main() {
    float total = 0;
    float preco = input();

    while (preco > 0) {
        total = total + preco;
        preco = input();
    }

    if (total > 50) {
        total = total * 0.9;
    }

    float pago = input();
    send(total, pago - total);
}

Caixa automático com estoque Abrir estoque

O pedido vira pares de preço, qtd (ChallengeSystem.gd:15-21). Este é o script de regime permanente: sensor() veio com Aprender sensor() e await() com Abrir estoque (builtins.gd:129), então o loop só é possível aqui. Deixe rodando e a árvore de upgrades se paga sozinha.

int main() {
    while (1) {
        if (sensor("cliente_na_tela")) {
            float total = 0;
            float preco = input();

            while (preco > 0) {
                float qtd = input();
                total = total + preco * qtd;
                preco = input();
            }

            if (total > 50) {
                total = total * 0.9;
            }

            float pago = input();
            send(total, pago - total);
        }
        await(0.1);
    }
}

O while (1) não trava o jogo: o escalonador dá 200 operações por frame a cada script (ScriptRuntimeManager.gd:16). O await(0.1) devolve o resto do orçamento para os outros scripts. sensor("cliente_na_tela") volta a false dentro do próprio submit(), então não há risco de atender o mesmo cliente duas vezes.

Delivery Online aba Delivery

Precisa rodar na aba Delivery: _validate_runtime_context compara o script_id com o da aba e recusa qualquer outro (DeliverySystem.gd:364-370). O lucro de cada categoria é lucro = 2 × lucro + base repetido uma vez por entrega (DeliverySystem.gd:210-214), com bases fixas [2, 4, 7] (DeliveryConfig.gd:5).

int lucro(int n, int b) {
    if (n == 0) {
        return 0;
    }
    return 2 * lucro(n - 1, b) + b;
}

int main() {
    int bases[3];
    int entregas[3];
    int lucros[3];
    int i = 0;

    bases[0] = 2;
    bases[1] = 4;
    bases[2] = 7;

    entregas = get_deliveries();

    while (i < 3) {
        lucros[i] = lucro(entregas[i], bases[i]);
        i = i + 1;
    }

    declare_profit(lucros);
}

Os quatro requisitos do validador estão todos aqui (DeliveryProgramValidator.gd:78-94): uma função fora da main, recursão com if e caso-base que retorna sem se chamar, um laço, e declare_profit() recebendo um array declarado com tamanho literal 3. int entregas[3] precisa existir antes do =: get_deliveries() devolve o array inteiro, mas atribuir a uma variável não declarada é erro de runtime.

Zerar, do save novo

Sequência completa
  1. Caixa de soma até R$ 100 → Aprender if(). Depois Aprender while(), Aprender sensor(), Cliente com troco, Abrir estoque, trocando o script a cada compra.
  2. Com o caixa automático rodando, comprar premium 1-3 e marketing 1-3 e manter o estoque cheio com buy_stock() para o bônus de 1,5×.
  3. R$ 370 no Delivery Online. Rodar o script da aba Delivery a cada relatório: 5 aprovações, 5 diamantes, 60 s de cooldown entre elas (contornável pela falha 10).
  4. Comprar Zerar.

Ordem de correção por custo-benefício

CorreçãoOndeEsforço
Envolver o cheat e o hotspot em OS.is_debug_build()TransactionManager.gd:74
game.gd:106-119
Trivial
Usar dinheiro_minimo em verificar_desbloqueios() ou remover o campoUpgradeData.gdTrivial
Aceitar corpo sem chaves em if/while/for, ou recusar no parser com erro legívelexecutor.gd:389Trivial
Validar o programa antes dos valores e limitar tentativas por relatórioDeliverySystem.gd:127Pequeno
Recompensa proporcional ao pedido; bônus 1,5× uma vez por reposiçãoChallengeSystem.gd:33Pequeno
Contar cooldown por tempo acumulado em vez de unix timeDeliverySystem.gd:224Pequeno
HMAC no save e validar requer ao aplicar upgrades no loadSaveManager.gd
UpgradeManager.gd:184
Médio
Ligar o valor declarado ao retorno efetivo da função recursivaDeliveryProgramValidator.gdMédio