Ouija
5 minutes to read
We are given a binary file called 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]=53a9e0435f7c7041c557e9d4a8418cb6a916f339, for GNU/Linux 3.2.0, not stripped
Reverse engineering
If we open the binary in Ghidra, we will see this decompiled main
function in C:
int main() {
undefined8 local_78;
undefined8 local_70;
undefined8 local_68;
undefined4 local_60;
undefined2 local_5c;
undefined local_5a;
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;
local_78 = 0x6877644b7b544c5a;
local_70 = 0x665f6b615f796661;
local_68 = 0x6b6d7874675f6c67;
local_60 = 0x616c7375;
local_5c = 0x6667;
local_5a = 0x7d;
setvbuf(stdout, NULL, 2, 0);
enc_flag = strdup((char *) &local_78);
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;
}
The program will take this string:
$ python3 -q
>>> from pwn import p8, p16, p32, p64
>>> p64(0x6877644b7b544c5a) + p64(0x665f6b615f796661) + p64(0x6b6d7874675f6c67) + p32(0x616c7375) + p16(0x6667) + p8(0x7d)
b'ZLT{Kdwhafy_ak_fgl_gtxmkuslagf}'
And will decrypt it. However, it will take a long time (16 minutes and only 2 characters were decrypted):
$ 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
So instead, we must find another approach.
Analyzing the cipher
These lines of code are the ones that actually decrypt the above string:
int main() {
// ...
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;
}
So, we see that they distinguish between uppercase letters, lowercase letters and special characters. For letters, they substract key_copy
(which is 18). In some cases, they add 0x1a
before to wrap around. Hence, this is a substitution cipher.
Flag
We can do the same operations in the Python REPL and get the flag:
$ python3 -q
>>> flag = []
>>> enc_flag = 'ZLT{Kdwhafy_ak_fgl_gtxmkuslagf}'
>>> 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{Sleping_is_not_obfuscation}'