GuessMe Parte 2

“Random, really!?…”

Introdução

Aqui vamos explorar a vulnerabilidade relacionada ao uso de uma função Random inseguro.

A aplicação em si ainda é sobre adivinhar um número secreto em até 10 tentativas.

Se voltarmos uns passos e olhar a MainActivty podemos ver o seguinte trecho de código:

Integer userGuess = StringsKt.toIntOrNull(editText.getText().toString());
if (userGuess != null) {
    this.attempts++;
    if (userGuess.intValue() < this.secretNumber) {
        displayMessage("Too low! Try again.");
    } else if (userGuess.intValue() > this.secretNumber) {
        displayMessage("Too high! Try again.");
    } else {
        displayMessage("Congratulations! You guessed the correct number " + this.secretNumber + " in " + this.attempts + " attempts.");
        disableInput();
    }
    if (this.attempts == this.maxAttempts) {
        displayMessage("Sorry, you've run out of attempts. The correct number was " + this.secretNumber + '.');
        disableInput();
        return;
    }

Esse trecho acima mostra basicamente como funciona a lógica por trás do jogo. Um ponto muito importante e que no primeiro momento eu não havia notado, foi sobre as mensagens que avisam se um número está mais alto ou mais baixo que o número correto.

Mas seguimos nossa análise do código que ao tentar descobrir como é gerado o “secretNumber” nos deparamos com o seguinte cenário.

public static final int TYPE_TARGET = 101;
 ...
private final int maxAttempts = 10;
 ...
private final void startNewGame() {
    this.secretNumber = Random.INSTANCE.nextInt(1, TypedValues.TYPE_TARGET);
    this.attempts = 0;
    TextView textView = this.resultTextView;
    EditText editText = null;
    if (textView == null) {
        Intrinsics.throwUninitializedPropertyAccessException("resultTextView");
        textView = null;
    }
    textView.setText("Guess a number between 1 and 100");
    EditText editText2 = this.guessEditText;
    if (editText2 == null) {
        Intrinsics.throwUninitializedPropertyAccessException("guessEditText");
    } else {
        editText = editText2;
    }
    editText.getText().clear();
    enableInput();
}

Podemos assumir algumas coisas após esse code review :P

  1. O range de valores que vão ser gerados vai de 1 ao 100
  2. O número máximo de tentativas é de 10
  3. A aplicação está usando o Random e não o SecureRandom.
  4. Quando não há um seed definido por padrão o timestamp é utilizado

Então basicamente quando começamos um “novo jogo”, o app entra nessa função zera tudo e gera um novo número secreto baseado no timestamp na hora em que essa função foi executada.

Se conseguirmos saber a hora em quem foi gerado esse número podemos acertar qual o seed que foi utilizado e consequentimente o valor secreto.

Explicação Random

O porque o Random é inseguro e porque eu deveria acreditar nesta afirmação!? Aqui definimos duas váriáveis em duas instâncias diferentes, porém utilizando o mesmo seed. Mesmo sendo variáveis diferentes, é gerado o mesmo valor!


Já nesse segundo exemplo utilizando o SecureRandom, realizei o mesmo processo e usando o mesmo seed e mesmo assim os dois valores são completamente diferentes.



Random + timestamp

Eu confesso que eu não consegui criar um jeito extremamente eficiente para resolver dessa forma, com certeza pelo jeito que desenvolvi a aplicação acaba tendo um delay muito grande entre a hora que é gerado o seed da aplicação e o meu seed utilizado para realizar o ataque.

Criei uum script em java para automatizar essa tarefa de ficar tentando adicinhar o número, muito do código utiliza comandos de sistema, pois trabalhar com adb shell não tem preço!

Código java 1

Você pode olhar o código e ver com calma, porém os pontos que queria ressaltar aqui são:

  1. Utilizando um emulador os logs são mais verbosos por padrão.
  2. Existe uma diferença gigantesca entre usar o CurrentTimeMillis do sistema e a hora que é gerado o log.
  3. É possível ler os dados da interface da aplicação através de um comando dump.

Códgo timestamp

Como esse código é curto resolvi colocar aqui para explicar que é possível ler no logcat quando que um comando de tecla enter foi acionado e puxar exatamente qual foi o timestamp dessa execução.

#!/bin/bash
logTime=$(adb logcat -d | grep "keyCode=KEYCODE_ENTER" | tail -n 1 |awk '{print $2}')
init_date=$(gdate -d "$logTime" +"%s%3N")
echo $init_date

A partir daí esse número foi o que eu gerei como valor inicial para as minhas seeds. A parte de ler o UI, foi para que eu soubesse dizer para meu script quando acabou as tentativas e o número não foi encontrado e quando foi encontrado o número correto antes de acabar as tentativas.

Basicamente é possível fazer o dump de um xml com todas as informações da tela como mensagens, campo habilitado, campo desabilitado, etc..

Esse é o trecho do código que realiza o parsing e a partir daí eu consgio ler os valores. Primeiro eu faço o dumo, salvo local e logo em seguida realizo o parse do XML.

public static boolean uiDisplayCheck(String[] args) {
boolean foundDisabled = false;
    try {
        Process process = Runtime.getRuntime().exec("adb shell uiautomator dump /data/local/tmp/window.xml");
        process.waitFor();

        process = Runtime.getRuntime().exec("adb pull /data/local/tmp/window.xml .");
        process.waitFor();

        File xmlFile = new File("window.xml");
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        Document doc = dBuilder.parse(xmlFile);
        doc.getDocumentElement().normalize();

O resto é só lógica de programação. :)


Random + busca binária

Insatisfeito com a porcentagem de acertos do primeiro script eu resolvi então apelar!

Como a aplicação me retorna se um valor estava mais alto ou mais baixo do esperado, eu pensei porque não juntar isso com meu guessing e transformar minha busca para que toda vez que eu fosse gerar um número random eu pudesse estar mais próximo do resultado!

Foi ai que eu resolvi aplicar a busca binária alterando o valor utilizado como inicial e final na minha função Random, isso tudo lendo a interface da tela do app.

Quem disse que excesso de mensagem (verbose) é vulnerabilidade somente em API!?

Esse é o trecho do código que faz esse paranauê.

    public static int generateRandomString(Random random,int lowEnd, int highEnd) throws IOException {
	int number = 0;
        number = random.nextInt(lowEnd, highEnd);
	System.out.println("The Random range: " + lowEnd + " " + highEnd);

Agora a seed do Random não importa mais uma vez que eu altero o range entre o início e o fim o valor da seed tanto faz, eu só mantive por comodidade mesmo.

Código java 2

Esse foi o resultado, muito mais acertivo e satisfatório!