Ouija
5 minutos de lectura
Se nos proporciona un binario llamado ouija
:
$ file ouija
ouija: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2cace162c306a34dcfc4837d648d047e2ea339fe, for GNU/Linux 3.2.0, not stripped
Ingeniería inversa
Si lo abrimos en Ghidra, veremos el código en C descompilado de la función main
:
int main() {
int key_copy;
int r;
int m;
int o;
int n;
int q;
int p;
int l;
char *enc_flag;
int k;
int j;
int i;
setvbuf(stdout, NULL, 2, 0);
enc_flag = strdup("ZLT{Svvafy_kdwwhk_lg_qgmj_ugvw_escwk_al_wskq_lg_ghlaearw_dslwj!}");
puts("Retrieving key.");
sleep(10);
for (i = 1; i < 0x1e; i = i + 1) {
if (i % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
key_copy = key;
puts("Hmm, I don\'t like that one. Let\'s pick a new one.");
sleep(10);
for (j = 1; j < 0x1e; j = j + 1) {
if (j % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
key_copy = key_copy + 5;
puts("Yes, 18 will do nicely.");
sleep(10);
for (k = 1; k < 0x14; k = k + 1) {
if (k % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
puts("Let\'s get ready to start. This might take a while!");
sleep(10);
for (l = 1; l < 0x32; l = l + 1) {
if (l % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
for (; *enc_flag != '\0'; enc_flag = enc_flag + 1) {
if ((*enc_flag < 'a') || ('z' < *enc_flag)) {
if ((*enc_flag < 'A') || ('Z' < *enc_flag)) {
puts("We can leave this one alone.");
sleep(10);
for (m = 1; m < 10; m = m + 1) {
if (m % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
} else {
puts("This one\'s an uppercase letter!");
sleep(10);
for (n = 1; n < 0x14; n = n + 1) {
if (n % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
if (*enc_flag - key_copy < 0x41) {
puts("Wrapping it round...");
sleep(10);
for (o = 1; o < 0x32; o = o + 1) {
if (o % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
*enc_flag = *enc_flag + '\x1a';
}
*enc_flag = *enc_flag - (char) key_copy;
}
} else {
puts("This one\'s a lowercase letter");
sleep(10);
for (p = 1; p < 0x14; p = p + 1) {
if (p % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
if (*enc_flag - key_copy < 0x61) {
puts("Wrapping it round...");
sleep(10);
for (q = 1; q < 0x32; q = q + 1) {
if (q % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
*enc_flag = *enc_flag + '\x1a';
}
*enc_flag = *enc_flag - (char) key_copy;
}
puts("Okay, let\'s write down this letter! This is a pretty complex operation, you might want to check back later.");
sleep(10);
for (r = 1; r < 300; r = r + 1) {
if (r % 5 == 0) {
printf("\r ");
}
putchar('.');
sleep(1);
}
puts(" done!");
printf("%c\n", (ulong) (uint) (int) *enc_flag);
}
puts("You\'re still here?");
return 0;
}
El programa cogerá esta cadena de texto:
ZLT{Svvafy_kdwwhk_lg_qgmj_ugvw_escwk_al_wskq_lg_ghlaearw_dslwj!}
Y la descifrará. Sin embargo, tomará mucho tiempo (16 minutos para solo 2 caracteres):
$ time ./ouija
Retrieving key.
..... done!
Hmm, I don't like that one. Let's pick a new one.
..... done!
Yes, 18 will do nicely.
..... done!
Let's get ready to start. This might take a while!
..... done!
This one's an uppercase letter!
..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
..... done!
H
This one's an uppercase letter!
..... done!
Wrapping it round...
..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
..... done!
T
This one's an uppercase letter!
..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
...^C
./ouija 0,02s user 0,03s system 0% cpu 16:46,49 total
Por tanto, tenemos que encontrar otra manera.
Analizando el cifrado
Estas son las líneas de código que realizan las operaciones criptográficas:
int main() {
// ...
enc_flag = strdup("ZLT{Svvafy_kdwwhk_lg_qgmj_ugvw_escwk_al_wskq_lg_ghlaearw_dslwj!}");
// ...
key_copy = key;
// ...
key_copy = key_copy + 5;
puts("Yes, 18 will do nicely.");
// ...
for (; *enc_flag != '\0'; enc_flag = enc_flag + 1) {
if ((*enc_flag < 'a') || ('z' < *enc_flag)) {
if ((*enc_flag < 'A') || ('Z' < *enc_flag)) {
puts("We can leave this one alone.");
// ...
} else {
puts("This one\'s an uppercase letter!");
// ...
if (*enc_flag - key_copy < 0x41) {
puts("Wrapping it round...");
// ...
*enc_flag = *enc_flag + '\x1a';
}
*enc_flag = *enc_flag - (char) key_copy;
}
} else {
puts("This one\'s a lowercase letter");
// ...
if (*enc_flag - key_copy < 0x61) {
puts("Wrapping it round...");
// ...
*enc_flag = *enc_flag + '\x1a';
}
*enc_flag = *enc_flag - (char) key_copy;
}
// ...
}
puts("You\'re still here?");
return 0;
}
Vemos que se distingue entre letras mayúsculas, letras minúsculas y caracteres especiales. Para las letras, restan key_copy
(que es 18). En algunos casos, añaden 0x1a
primero para no salirse del rango de mayúsculas/minúsculas. Por tanto, estamos ante un cifrado de sustitución.
Flag
Podemos realizar las mismas operaciones en el REPL de Python para obtener la flag:
$ python3 -q
>>> flag = []
>>> enc_flag = 'ZLT{Svvafy_kdwwhk_lg_qgmj_ugvw_escwk_al_wskq_lg_ghlaearw_dslwj!}'
>>> for c in enc_flag:
... if c.isalpha() and c.isupper():
... flag.append(chr(ord(c) - 18 + 0x1a) if ord(c) - 18 < 0x41 else chr(ord(c) - 18))
... if c.isalpha() and c.islower():
... flag.append(chr(ord(c) - 18 + 0x1a) if ord(c) - 18 < 0x61 else chr(ord(c) - 18))
... if not c.isalpha():
... flag.append(c)
...
>>> ''.join(flag)
'HTB{Adding_sleeps_to_your_code_makes_it_easy_to_optimize_later!}'