Introducción al Reversing – 0x10 Baby’s RE from FlareON con amor ❤️

Espectativas

Con la resolución de los retos más sencillos baby’s (constan del primer) de todos los flare on, aprenderemos:

  • Crackmes con algoritmos muy básicos.
  • Codificación y decodificación en aplicaciones .NET.
  • Ingeniería inversa de código .NET y en binarios Windows.
  • Sintaxis intel x86. Basics of ASM.
  • Manejo básico de IDA, x64dbg y Ghidra.
  • Identificación de algoritmos de codificación reales, tal como base64.
  • Identificación de algoritmos de cifrado tal como ROT13.
  • Reversing Javascript y uso del depurador del navegador.
  • Reversing Java. Decompilar un .jar con Procyon.
  • Reversing código fuente Python.

Challenge 1 FlareON 2014

Analizaremos un binario desarrollado en .NET. El binario es muy sencillo, únicamente realiza una decodificación y lo muestra en el Form1. Además de cambiar una imagen el output son caracteres ASCII no imprimibles. Para poder analizar este binario, tendremos que usar el software dnSpy.

La función que nos interesa es la siguiente:

// Token: 0x06000002 RID: 2 RVA: 0x00002060 File Offset: 0x00000260
		private void btnDecode_Click(object sender, EventArgs e)
		{
			this.pbRoge.Image = Resources.bob_roge;
			byte[] dat_secret = Resources.dat_secret;
			string text = "";
			foreach (byte b in dat_secret)
			{
				text += (char)((b >> 4 | ((int)b << 4 & 240)) ^ 41);
			}
			text += "\0";
			string text2 = "";
			for (int j = 0; j < text.Length; j += 2)
			{
				text2 += text[j + 1];
				text2 += text[j];
			}
			string text3 = "";
			for (int k = 0; k < text2.Length; k++)
			{
				char c = text2[k];
				text3 += (char)((byte)text2[k] ^ 102);
			}
			this.lbl_title.Text = text3;
		}

El botón encargado de decodificar el secret una vez realiza la acción, la aplicación no hace más nada, teniendo que cerrarla.

Este reto es muy sencillo, solo tenemos que realizar lo siguiente:

  • Abrir dnSpy.
  • Poner un punto de ruptura (breakpoint) en la función previamente mostrada.
  • El primer foreach intera en el array dat_secret realizando una serie de operaciones aritmeticas y añadiendola en la bariable text. Esa variable text contiene la flag. Easy. Baby!!

Challenge 1 FlareON 2015

Este reto muy sencillo es un crackme. Como crackme que es, nos indica por pantalla una password. Si nos fijamos en el código principal se identifica rápido una operación xor realizado a nuestro primer carácter del input y luego comparándose con un byte, siendo una key.

Key:

Con este sencillo código resolvemos el reto sin necesidad de tener que debuggearlo a mano.

import random
import sys

def check_key(key):
    char_sum = 0
    for c in key:
        char_sum += ord(c)
    sys.stdout.write("{0:3} | {1}      \r".format(char_sum, key))
    sys.stdout.flush()
    return char_sum


#REVERSE ENGINEERING

key_ = [0x1f, 0x08, 0x13, 0x13, 0x04, 0x22, 0x0e, 0x11, 
0x4d,0x0d,0x18,0x3d,0x1b,0x11,0x1c,0x0f,0x18,0x50,0x12,0x13,0x53,0x1e,0x12,0x10]


def code():
    i = 0
    list_ = []
    while True:
        inp = ""
        inp += random.choice("""abcdefghijklmnopqrst
        uvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_!$%&#'()*+,./:;<>=?@[]`{~}\"""")
        inpu = check_key(inp)
        xor = inpu ^ 0x7d
        if xor == key_[i]:
            list_.append(chr(inpu))
            if key_[i] == 0x10:
                return list_
            i += 1

if __name__ == "__main__":
    hardcoded_check = code()                      
    print("Flag -->"+"".join(hardcoded_check)) 

Output:

Challenge 1 FlareON 2016

Este reto es un poco mas complicado que el anterior, pero basado en el mismo tipo: Crackme con un algoritmo que tenemos que «crackear». En este punto nos tenemos que parar a pensar, en identificar algoritmos, no todos los algoritmos de cifrado o de codificación son «custom».

Ahora no tenemos un simple xor, sino una función encargada de realizar la operación de iterar con nuestro input. Debido a la «complejidad» del algoritmo de comprobación, usaremos el software «Ghidra» para ofrecerme la mejor experiencia de usuario en cuanto a decompilers ❤️

Abrimos Ghidra, y la función encargada del cifrado o codificación tiene código muy parecido a la codificación de base64. Además de esto, cabe destacar que al abrir el debugger y poner un breakpoint en la llamada a la función encargada de realizar la codificación de la password, tenemos un base64.

Muy fácil no? Lo ponemos en cyberchef y resulta que la decodificación nos da caracteres no imprimibles y sin sentido alguno. Simplemente al entrar en la función vemos el primer error en la implementación de base64! Tenemos la tabla de codificación ligeramente «desplazada», haciendo que la codificación no sea la habitual. Si, tenemos un base64 ligeramente modificado.

La tabla de codificación en un base64 normal, es el siguiente:

static char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
                                'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
                                'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
                                'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
                                'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
                                'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
                                'w', 'x', 'y', 'z', '0', '1', '2', '3',
                                '4', '5', '6', '7', '8', '9', '+', '/'};

Sin embargo nosotros tenemos este:

Se identifica además muy rápido la diferencia e identificación de que es base64, en el siguiente código:

  • Correcto:
encoded_data[j++] = encoding_table[(triple >> 3 * 6) & 0x3F];
encoded_data[j++] = encoding_table[(triple >> 2 * 6) & 0x3F];
encoded_data[j++] = encoding_table[(triple >> 1 * 6) & 0x3F];
encoded_data[j++] = encoding_table[(triple >> 0 * 6) & 0x3F];
  • Incorrecto:
      *(char *)((int)pvVar2 + local_c) =
           s_ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabc_00413000[uVar3 >> 0x12 & 0x3f];
      *(char *)((int)pvVar2 + local_c + 1) =
           s_ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabc_00413000[uVar3 >> 0xc & 0x3f];
      *(char *)((int)pvVar2 + local_c + 2) =
           s_ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabc_00413000[uVar3 >> 6 & 0x3f];
      *(char *)((int)pvVar2 + local_c + 3) =
           s_ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabc_00413000[uVar3 & 0x3f];

Resolución. Ya que tenemos identificado el problema podemos resolver usando un código en C de implementación de base64 y modificar la tabla de codificación por la previa vista. Finalmente el código que resuelve el reto:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
 
static char encoding_table[] = {'Z','Y','X','A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
                                'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
                                'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'z',
                                'y', 'x', 'a', 'b', 'c', 'd', 'e', 'f',
                                'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
                                'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
                                'w', '0', '1', '2', '3',
                                '4', '5', '6', '7', '8', '9', '+', '/'};
static char *decoding_table = NULL;
static int mod_table[] = {0, 2, 1};
 
void build_decoding_table() {
 
    decoding_table = malloc(256);
 
    for (int i = 0; i < 64; i++)
        decoding_table[(unsigned char) encoding_table[i]] = i;
}
 
 
void base64_cleanup() {
    free(decoding_table);
} 
 
char *base64_encode(const unsigned char *data,
                    size_t input_length,
                    size_t *output_length) {
 
    *output_length = 4 * ((input_length + 2) / 3);
 
    char *encoded_data = malloc(*output_length);
    if (encoded_data == NULL) return NULL;
 
    for (int i = 0, j = 0; i < input_length;) {
 
        uint32_t octet_a = i < input_length ? (unsigned char)data[i++] : 0;
        uint32_t octet_b = i < input_length ? (unsigned char)data[i++] : 0;
        uint32_t octet_c = i < input_length ? (unsigned char)data[i++] : 0;
 
        uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
 
        encoded_data[j++] = encoding_table[(triple >> 3 * 6) & 0x3F];
        encoded_data[j++] = encoding_table[(triple >> 2 * 6) & 0x3F];
        encoded_data[j++] = encoding_table[(triple >> 1 * 6) & 0x3F];
        encoded_data[j++] = encoding_table[(triple >> 0 * 6) & 0x3F];
    }
 
    for (int i = 0; i < mod_table[input_length % 3]; i++)
        encoded_data[*output_length - 1 - i] = '=';
 
    return encoded_data;
}
 
 
unsigned char *base64_decode(const char *data,
                             size_t input_length,
                             size_t *output_length) {
 
    if (decoding_table == NULL) build_decoding_table();
 
    if (input_length % 4 != 0) return NULL;
 
    *output_length = input_length / 4 * 3;
    if (data[input_length - 1] == '=') (*output_length)--;
    if (data[input_length - 2] == '=') (*output_length)--;
 
    unsigned char *decoded_data = malloc(*output_length);
    if (decoded_data == NULL) return NULL;
 
    for (int i = 0, j = 0; i < input_length;) {
 
        uint32_t sextet_a = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
        uint32_t sextet_b = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
        uint32_t sextet_c = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
        uint32_t sextet_d = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
 
        uint32_t triple = (sextet_a << 3 * 6)
        + (sextet_b << 2 * 6)
        + (sextet_c << 1 * 6)
        + (sextet_d << 0 * 6);
 
        if (j < *output_length) decoded_data[j++] = (triple >> 2 * 8) & 0xFF;
        if (j < *output_length) decoded_data[j++] = (triple >> 1 * 8) & 0xFF;
        if (j < *output_length) decoded_data[j++] = (triple >> 0 * 8) & 0xFF;
    }
 
    return decoded_data;
}
 
int main(){
    
    char * encoded_data = "x2dtJEOmyjacxDemx2eczT5cVS9fVUGvWTuZWjuexjRqy24rV29q";
    
    
    long decode_size = strlen(encoded_data);
    char * decoded_data = base64_decode(encoded_data, decode_size, &decode_size);
    printf("Decoded Data is: %s \n",decoded_data);
    exit(0);
}

Output:

Lecciones aprendidas:

  • No estudiar al detalle implementaciones de algoritmos, ya que es posible que sea un algoritmo ya existente, y simplemente con identificarlo nos sirve.
  • Replicar el código de decompilado de Ghidra en C.

Challenge 1 FlareON 2017

En este reto usaremos el navegador Firefox para depurar código Javascript. Es una página HTML con código .js donde comprueba la flag. Este sería el código:

document.getElementById("prompt").onclick = function () {
                var flag = document.getElementById("flag").value;
                var rotFlag = flag.replace(/[a-zA-Z]/g, function(c){return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);});
                if ("PyvragFvqrYbtvafNerRnfl@syner-ba.pbz" == rotFlag) {
                    alert("Correct flag!");
                } else {
                    alert("Incorrect flag, rot again");
                }
            }

El algoritmo a simple vista identificamos que es ROT13. De igual modo si introducimos «aaaa» y ponemos un punto de ruptura y avanzamos hasta la estructura de control, vemos que la variable rotFlag ahora vale «nnnn»:

Si comprobamos en CyberChef, estamos en lo cierto, es ROT13. Cogemos la string hardcodeada: PyvragFvqrYbtvafNerRnfl@syner-ba.pbz y lo ponemos en CyberChef y así obtener en claro la flag. Baby!

Challenge 1 FlareON 2018

Este reto es un JAR así podemos decompilarlo online en esta página. Es importante usar la opcion de decompilador Procyon.

Con abrirlo ya tendríamos la flag, demasiado baby 😅.

public class InviteValidator
{
    public static void main(final String[] args) {
        final String response = JOptionPane.showInputDialog(null, "Enter your invitation code:", "Minesweeper Championship 2018", 3);
        if (response.equals("GoldenTicket2018@flare-on.com")) {
            JOptionPane.showMessageDialog(null, "Welcome to the Minesweeper Championship 2018!\nPlease enter the following code to the ctfd.flare-on.com website to compete:\n\n" + response, "Success!", -1);
        }
        else {
            JOptionPane.showMessageDialog(null, "Incorrect invitation code. Please try again next year.", "Failure", 0);
        }
    }
}

Challenge 1 FlareON 2019

Tenemos otra aplicación .NET. Solo con abrirlo podemos identificar dos Stages, dos fases. La primera esta referida a si introducimos la string RAINBOW, comenzara la animación. Ademas el nombre del método es referido al boton de fuego.

private void FireButton_Click(object sender, EventArgs e)
		{
			if (this.codeTextBox.Text == "RAINBOW")
			{
				this.fireButton.Visible = false;
				this.codeTextBox.Visible = false;
				this.armingCodeLabel.Visible = false;
				this.invalidWeaponLabel.Visible = false;
				this.WeaponCode = this.codeTextBox.Text;
				this.victoryAnimationTimer.Start();
				return;
			}
			this.invalidWeaponLabel.Visible = true;
			this.codeTextBox.Text = "";
		}

El segundo stage tenemos otro método para verificar el código weapon.

private bool isValidWeaponCode(string s)
		{
		char[] array = s.ToCharArray();
		int length = s.Length;
		for (int i = 0; i < length; i++)
		{
			char[] array2 = array;
			int num = i;
			array2[num] ^= 'A';
		}
		return array.SequenceEqual(new char[]
		{
			'\u0003',
			' ',
			'&',
			'$',
			'-',
			'\u001e',
			'\u0002',
			' ',
			'/',
			'/',
			'.',
			'/'
		});
	}

Explicamos por partes:

  • En primera instancia se almacena la string que nosotros pasamos en un array.
  • Calcula la longitud.
  • Realiza un XOR con el carácter «A» como key. Como la operación es reversible tiene que dar de resultado el array SequenceEqual.
  • Introducimos entonces Bagel_Cannon.

Y finalmente obtenemos la flag:

Challenge 1 FlareON 2020

El primer reto corresponde a una aplicación en Python. Nos dan el binario para poder ejecutarlo, y además disponemos del código fuente. Si ejecutamos la aplicación lo primero que aparece es un cuadro de dialogo para introducir la password.

De momento podríamos pensar que solo habría que tomar la password como si fuese la flag, o, que si introducimos una password correcta nos devuelva la flag, o, simplemente es el acceso a la aplicación con más funcionalidades. Como no puede ser de otra manera entraremos en detalles mirando el código fuente.

Identificamos la función main() y primero comprueba la función password_screen(), es decir, si el valor de retorno de la función es correcto, entrará en la estructura de control, y sino, entrará en la función password_fail_screen(). Así de primeras podemos observar que el reto tiene más pinta de que la password es simplemente el acceso a la aplicación y no será inmediatamente la solución.

def main():
    if password_screen():
        game_screen()
    else:
        password_fail_screen()
    pg.quit()

if __name__ == '__main__':
    main()

Nos dirigimos entonces a password_screen(). Lo más importante es esta parte del código. Si resulta que la función password_check(input) es verdadero, devolverá True, lo cual es el resultado que nosotros queremos esperar si queremos que entre en game_screen().

if input_box.submitted:
            if password_check(input_box.text):
                return True
            else:
                return False

La función password_check(input), tenemos una string hardcodeada donde se realizará una operación aritmetica y devolverá dicha string siempre y cuando sea igual el input y la key.

def password_check(input):
    altered_key = 'hiptu'
    key = ''.join([chr(ord(x) - 1) for x in altered_key])
    return input == key

Solo con replicarnos este código en un interprete de python, tenemos que el input que debemos introducir como password es ghost.

Cat Gaming

Comenzamos el juego. Usando cheat engine podemos obtener el incremento de las current_coins.

Pero modificando el valor directamente, vemos que no se modifica en la interfaz GUI de la aplicación, por lo tanto no nos vale. De todas maneras tenemos el código fuente, simplemente con instalar pygame con pip ya podríamos ejecutar con python nuestra versión modificada del juego. ¿Qué tenemos que modificar?

  • Tenemos que modificar el Autoclicker, y que cuando compremos un autoclicker por el valor de 10 monedas, haga un incremento de 10000000000.

Para poder entrar en la función victory_screen() se tiene que cumplir que nuestras monedas coincida con un target_amount bastante elevado, y como no puede ser de otra manera no vamos a estar comprando autoclickers o dando al gato para incrementar las monedas.

target_amount = (2**36) + (2**35)
        if current_coins > (target_amount - 2**20):
            while current_coins >= (target_amount + 2**20):
                current_coins -= 2**20
            victory_screen(int(current_coins / 10**8))
            return

Modificamos esta parte del código dentro de la estructura de control buying (que es el comprar el autoclicker).

if buying:
            try:
                amount_to_buy = int(autoclickers_input.text)
            except:
                amount_to_buy = 1
                autoclickers_input.text = '1'
            if amount_to_buy > 0 and current_coins >= amount_to_buy * 10:
                current_coins -= amount_to_buy * 10
                current_autoclickers += amount_to_buy
            buying = False

Donde pone:

current_coins -= amount_to_buy * 10

Ponemos:

current_coins += amount_to_buy * 10000000000

Ya tenemos la cantidad deseada! Ahora solo con dar 10 veces a Buy ya obtendremos la cifra deseada y win!.

Podríamos resolverlo sin necesidad de modificar el código fuente, y sin duda más elegante. Sería obtener el token pasado a la función decode_flag(token) que trata de decodificar la flag.

Si resulta que el target_amount tiene un valor de 103079215104, para poder entrar en la estructura de control nuestras monedas tienen que se mayor a 103078166528.

target_amount = (2**36) + (2**35)
if current_coins > (target_amount - 2**20):
        while current_coins >= (target_amount + 2**20):
             current_coins -= 2**20
        print(current_coins)
        print(current_coins / 10**8)
        print(int(current_coins / 10**8))
        victory_screen(int(current_coins / 10**8))
        return

Teniendo en cuenta la división con 10**8 para todo valor que este comprendido con 103078166528, nos dará un float del tipo 1030.loquesea

No es un bug, pero esta intencionado de que coga un int del valor float de la división, por tanto ya sabemos que el token siempre será 1030 para cualquier valor comprendido en lo anterior. Ya lo tenemos entonces. Si ejecutamos la función, obtenemos la flag!.

def decode_flag(frob):
    last_value = frob
    encoded_flag = [1135, 1038, 1126, 1028, 1117, 1071, 1094, 1077, 1121, 1087, 1110, 1092, 1072, 1095, 1090, 1027,
                    1127, 1040, 1137, 1030, 1127, 1099, 1062, 1101, 1123, 1027, 1136, 1054]
    decoded_flag = []

    for i in range(len(encoded_flag)):
        c = encoded_flag[i]
        val = (c - ((i%2)*1 + (i%3)*2)) ^ last_value
        decoded_flag.append(val)
        last_value = c

    return ''.join([chr(x) for x in decoded_flag])


print(decode_flag(1030))