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
- O range de valores que vão ser gerados vai de 1 ao 100
- O número máximo de tentativas é de 10
- A aplicação está usando o Random e não o SecureRandom.
- 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!
Você pode olhar o código e ver com calma, porém os pontos que queria ressaltar aqui são:
- Utilizando um emulador os logs são mais verbosos por padrão.
- Existe uma diferença gigantesca entre usar o CurrentTimeMillis do sistema e a hora que é gerado o log.
- É 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.
Esse foi o resultado, muito mais acertivo e satisfatório!
Posts:
- Post Board parte 2
- Post Board parte 1
- Guess Me Parte 2
- Guess Me Parte 1
- Food Store
- IOT Connect
- UnCrackable L1 Parte 2
- UnCrackable L1 Parte 1
- CVE-2022-26352
- DCA php
- Docker Code Analyzer
- Vulnado Parte 3
- Vulnado Parte 2
- Vulnado Parte 1
- Damm Vulnerable WebSocket
- OWASP ZAP Zed Attack Proxy
- Estratégias de um code review
- O que é code review?