Seguimos los posts sobre Flare-On 2021 con un reto que es bastante de adivinar, donde se quedó mucha gente encallada un buen rato sin saber cómo seguir aunque, quitando la parte “guessy”, es un reto bastante sencillo.
En este reto nos dan un archivo llamado antioch.tar, una imagen de Docker que contiene un montón de layers con archivos .dat y un ejecutable ELF llamado AntiochOS. Lo primero que hice fue extraer el ELF y ejecutarlo.
Vemos que nos aparece una especie de consola que no tenemos ni idea de que hace, así que toca abrirlo con nuestro decompilador favorito y ver que comandos se pueden usar.
void start()
{
v0 = sub_4013E0(); // Calcula el texto de la version
write(1, v0, 37);
sub_4012E0(buff); // "Type help for help"
write(1, buff, 19);
while ( 1 )
{
write(1, ' >', 2);
if ( !read(0, buff, 0x80) ) // Leemos el input
break;
quit_string(cmd); // Obtenemos string del posible comando
if ( !memcmp(buff, cmd, 5) ) // Comparamos input con posible opcion
break;
help_string(cmd); // Obtenemos string del posible comando
if (memcmp(buff, cmd, 5) ) // Comparamos input con posible opcion
{
consult_string(cmd); // Obtenemos string del posible comando
if ( memcmp(buff, cmd, 8) ) // Comparamos input con posible opcion
{
approach_string(cmd); // Obtenemos string del posible comando
if ( !memcmp(buff, cmd, 9) ) // Comparamos input con posible opcion
approach();
}
else
{
consult();
}
}
else
{
print_help(); // Printamos el texto del comando "help"
}
}
exit(0);
}
void quit_string(char *cmd)
{
cmd[0] = 'q';
cmd[1] = 'u';
cmd[2] = 'i';
cmd[3] = 't';
cmd[4] = '\n';
cmd[5] = '\0';
}
Si analizamos el pseudocódigo de la función start() vemos que se trata de un bucle donde tenemos una serie de condiciones donde se compara el input con los posibles comandos devueltos por las funciones que he nombrado _string().
Todas estas funciones tienen el mismo formato como el que podemos ver en la función quit_string(), donde en el buffer argumento se le pone el valor de la string. Por lo tanto vemos que tenemos cuatro opciones, “quit”, “help”, “approach” o “consult”, de estas hay dos que no nos interesan, ya que solo cierran el programa o printan el texto de ayuda.
Nos centramos en “approach” y “consult” que llaman a las funciones que he nombrado approach() y consult() con mucha originalidad. Así que vamos a ejecutar otra vez el binario y vemos que resultado nos da al usar estos comandos.
Vemos que la opción “consult” nos saca una gran cantidad de "V"s sin ningún sentido y la opción “approach” nos hace una pregunta que en principio no sabemos resolver. Si nos paramos a pensar como un jugador de CTF, lo más posible es que si encontramos las respuestas correctas a las preguntas de la opción “approach”, al darle a la opción “consult” obtendremos la flag o al menos algo con sentido.
Volvemos ahora al decompilador a analizar la función “approach()” y a buscar nuestras respuestas.
void approach()
{
v1 = sub_401260(); // "Approach the Gorge of Eternal Peril!"
write(1, v1, 37);
sub_401120(v13); // "What is your name?"
write(1, v13, 19);
v2 = read(0, v14, 128u);
v3 = calc_crc32(v14, v2);
v4 = MATRIZ_CRC[1];
v5 = 0xB59395A9;
while ( v5 != v3 ) // [1]
{
v0 = (v0 + 1);
if ( v0 == 30 )
return write(1, "...AAARGH\n\n", 11);
v5 = *v4;
v4 += 3;
}
sub_401180(v13); // "What is your quest?"
write(1u, v13, 20);
if ( read(0, v14, 0x80u) > 1 )
{
sub_4011E0(v13); // "What is your favorite color?"
write(1, v13, 29);
v6 = read(0, v14, 0x80u);
v7 = calc_crc32(v14, v6);
v8 = MATRIZ_CRC[v0];
if ( v8[1] == v7 ) // [2]
{
v9 = v8[2]; // [3]
if ( v9 > 0 )
{
itoa(v9, v14); // [3]
v10 = sub_4012A0(); // "Right. Off you go. #"
write(1, v10, 20);
write(1, v14, strlen(v14)); // [3]
write(1, '\n', 1);
}
}
}
write(1, "...AAARGH\n\n", 11);
Mirando ahora el pseudocódigo de la función aproach() encontramos varias cosas interesantes.
En primer lugar, diversas funciones que nos devuelven texto como las que hemos visto anteriormente, he anotado el texto que devuelven con comentarios, y nos damos cuenta que hay tres preguntas en vez de una.
Otra función interesante es la que he nombrado calc_crc32() que como el nombre indica nos devuelve el crc32 del input.
Identificar este tipo de funciones es muy sencillo si se sabe la estructura de los algoritmos más conocidos para calcular un hash.
A continuación vemos un extracto de la implementación de crc32 copiado de Wikipedia y el que encontramos en el binario, vemos como son casi idénticos.
// Funcion copiada de Wikipedia
uint32_t CRC32(const uint8_t data[], size_t data_length) {
uint32_t crc32 = 0xFFFFFFFFu;
for (size_t i = 0; i < data_length; i++) {
const uint32_t lookupIndex = (crc32 ^ data[i]) & 0xff;
// CRCTable is an array of 256 32-bit constants
crc32 = (crc32 >> 8) ^ CRCTable[lookupIndex];
}
// Finalize the CRC-32 value by inverting all the bits
crc32 ^= 0xFFFFFFFFu;
return crc32;
}
// Funcion de AntiochOS
_int64 calc_crc32(char *a1, int a2)
{
if ( a2 <= 0 )
return 0;
v2 = &a1[a2 - 1 + 1];
v3 = -1;
do
{
v4 = *a1++;
v3 = dword_402260[(v3 ^ v4)] ^ (v3 >> 8);
}
while ( v2 != a1 );
return ~v3;
}
En el pseudocódigo de approach() también encontramos una referencia a una sección de memoria que he nombrado MATRIZ_CRC que se trata de una array con diversos enteros. A esta sección de memoria se hace referencia de tres enteros en tres, por lo tanto, deducimos que se trata de una especie de matriz de tres columnas.
Mirando el código vemos que el crc32 de la respuesta a la primera pregunta se compara con los valores de la primera columna de la matriz ([1]). Si coincide con alguno de estos, el crc32 de la tercera respuesta se compara con la segunda columna de la fila correspondiente al valor que ha coincidido previamente ([2]). Si este coincide se nos devuelve el valor de la tercera columna de la fila correspondiente ([3]). También vemos que a la segunda pregunta se le puede responder cualquier cosa.
Si ahora nos paramos a pensar podemos, o intentar hacer fuerza bruta de los crc32 o intentar valores relacionados con la película de Monty Python en la que se basa este reto.
En mi caso, hice un mix de las dos opciones y obtuve las dos respuestas ‘Sir Lancelot’ y ‘Blue’.
Una vez obtenido el primer par de respuestas correctas vemos que tan solo nos devuelve el entero que encontramos en la tercera columna de la matriz, así que no nos sirve de mucho para encontrar la flag.
Vamos a analizar la otra función consult() a ver si nos ayuda a entender algo.
void consult()
{
v0 = 'a';
v9 = '..dat';
memset(v11, 0, sizeof(v11));
v1 = sub_4010E0(); // "Consult the Book of Armaments!"
write(1, v1, 31);
do
{
while ( 1 )
{
v9[0] = v0;
v2 = open(v9); // [n].dat
v3 = v2;
if ( v2 >= 0 )
break;
if ( ++v0 == '{' )
goto LABEL_7;
}
read(v2, v10, 0x1000u);
close(v3);
v4 = v11;
v5 = v10;
do
*v4++ ^= *v5++;
while ( v4 != &v12 );
++v0;
}
while ( v0 != 123 );
LABEL_7:
if ( !xmmword_404100 )
{
xmmword_404100 = _mm_load_si128(&xmmword_402240);
xmmword_404110 = xmmword_404100;
xmmword_404120 = xmmword_404100;
xmmword_404130 = xmmword_404100;
xmmword_404140 = xmmword_404100;
xmmword_404150 = xmmword_404100;
xmmword_404160 = xmmword_404100;
xmmword_404170 = xmmword_404100;
xmmword_404180 = xmmword_404100;
xmmword_404190 = xmmword_404100;
xmmword_4041A0 = xmmword_404100;
xmmword_4041B0 = xmmword_404100;
xmmword_4041C0 = xmmword_404100;
xmmword_4041D0 = xmmword_404100;
xmmword_4041E0 = xmmword_404100;
xmmword_4041F0 = xmmword_404100;
sub_401000(&xmmword_404100);
}
for ( i = 0; i != 4096; ++i )
{
v7 = '\n';
if ( (i & 0xF) != 15 )
v7 = *(&xmmword_404100 + v11[i]);
v11[i] = v7;
}
return write(1u, v11, 4096);
}
Sin analizar muy profundamente esta función vemos que está leyendo los diversos archivos .dat que veíamos en las layers y realizando un XOR con sus contenidos antes de usarlo para printar las "V"s que aparecian en consola. Esto nos da la idea de mirar con más detalle las diversas layers que teníamos en la imagen de Docker.
Si nos fijamos en los authors de cada una de las layers vemos que son nombres de la película, Greeen Knight, Sir Gallahad, etc. Si ahora calculamos los crc32 de estos authors vemos que coinciden con valores de la primera columna de la matriz. Sin necesidad de calcular el crc32 de la segunda columna podemos relacionar author con el entero de la tercera columna.
Ahora la pregunta donde mucha gente se quedó encallada es que hacer con esta relación. Sabemos que la función consult() utiliza los archivos .dat para algo, así que en un momento de inspiración se nos tiene que ocurrir extraer las layers correspondientes con cada author en el orden indicado por el entero. Para eso hice este feísimo script en Python, donde name_order contiene los nombres de los authors en el orden indicado por el entero.
import os
import tarfile
rootdir=(r'C:\Users\User\Desktop\03_antioch')
for name in name_order:
for folder, dirs, files in os.walk(rootdir):
for file in files:
if file == "json":
fullpath = os.path.join(folder, file)
with open(fullpath, 'r') as f:
for line in f:
if name.decode('utf-8') in line:
print(i,folder)
tar_file = tarfile.open(folder+r"\layer.tar")
tar_file.extractall(r'C:\Users\User\Desktop\unpacket_layers')
tar_file.close()
break
Si ahora ejecutamos el binario AntiochOS en la carpeta con los .dat extraídos en orden, y usamos el comando consult() veremos que obtenemos la flag en un precioso ASCII art.
Contribución gracias a @0xGsch
Comentarios
Publicar un comentario