Though we didn’t spend much time on the LA CTF, the challenges were of good quality. Here is a writeup of the-eye, a reverse challenge that I found very interesting.

the-eye

rev/the-eye

Author : aplet123

I believe we’ve reached the end of our journey. All that remains is to collapse the innumerable possibilities before us.

nc chall.lac.tf 31313

We were given a Dockerfile and an executable :

$ ./the-eye 
msg.txt is missing

$ echo "1234" > msg.txt && ./the-eye 
3421

The program needs a msg.txt file to execute, and shuffles it’s content before printing it out. Let’s try connecting to the server :

$ nc chall.lac.tf 31313
rheehtrt  i thna e  itri2neldueptbeoit_hl y tEr,taresnn  -g eiiilsulia comteeaat,cg o t Trsgnate_aaau;e}mt2Eussi hn   p  gr astdaseffrghahortoextys. u s  loaaaHitticedomiistet,hngspti ierrstcaysyctt aseitssmtou seolier tdte r, nyenw tsnyti ep fsi a al   ocei acnrendprhWhnit aasi naen seipehenar   tliestsodt y  n   hw ae a sett tntreroeauiiympdsgde_uoai{htoenc ndnlee cd.  aagttnr tstidsa,lehlap axloNdrle lna neegmsup rsoleee ootgyc sdnre npmganieav  oolsee,nOtlealout xahhvbe ee igrt peh,e tfrrnencaidi foeensyhvudm ess_sm  esgmhann exacosda essecfute n_eowelehtarohm ce  cntsr-hyleehgpsepeacyt_ ulnsea m dmshaedH ldeofdaye eo.cxslA hun  vtensel aeia ena  .vseelo nwhrohtdervc aednsi odpameseemhtn_og hne op?pi  rr  ttfirWd r

It seems like in order to solve this challenge we’ll need to get the original message, which will probably include the flag. Let’s try and reverse it using Ghidra.

Static Analysis

undefined8 main(void)
{
  time_t curr_time;
  char *message;
  undefined4 i;
  
  curr_time = time((time_t *)0x0);
  srand((uint)curr_time);
  message = (char *)read_msg();
  for (i = 0; i < 22; i = i + 1) {
    shuffle(message);
  }
  puts(message);
  free(message);
  return 0;
}

The main() function is using the current time as a seed for the rand function : srand((uint)curr_time);.

It is then getting the content of the msg.txt file using the read_msg() function, before calling shuffle() 22 times :

void shuffle(char *message)
{
  int random;
  size_t len;
  int i;
  char curr_char;
  int length;
  
  len = strlen(message);
  length = (int)len;
  while (i = length + -1, -1 < i) {
    random = rand();
    curr_char = message[i];
    message[i] = message[random % length];
    message[random % length] = curr_char;
    length = i;
  }
  return;
}

shuffle() swaps the last byte from message with another random one determined using rand(). It’s repeating this operation on every byte of the message, from the last byte to the first one.

Once this encoding done 22 times as mentionned earlier, the main function prints out the encoded message : puts(message);.

Since the whole operation is using a pseudo-random generator with a known seed, we could replicate this randomness, and decode the message by inverting this algorithm.

Scripting

First things first, let’s break the randomness :

from ctypes import CDLL

if __name__ == "__main__":
    libc = CDLL("/usr/lib64/libc.so.6")
    curr_time = libc.time(None)
    libc.srand(curr_time)

Using CDLL to import a libc we can execute C functions from a Python script. This way we can execute the same functions as the program and get the same outputs. We can also dynamically check whether this is working using GDB.

First let’s get the output of time() :

───────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555555370 <main+0004>      sub    rsp, 0x10
   0x555555555374 <main+0008>      mov    edi, 0x0
●  0x555555555379 <main+000d>      call   0x5555555550a0 <time@plt>
 → 0x55555555537e <main+0012>      mov    edi, eax
   0x555555555380 <main+0014>      call   0x555555555080 <srand@plt>
   0x555555555385 <main+0019>      mov    eax, 0x0
   0x55555555538a <main+001e>      call   0x5555555551f9 <read_msg>
   0x55555555538f <main+0023>      mov    QWORD PTR [rbp-0x10], rax
   0x555555555393 <main+0027>      mov    DWORD PTR [rbp-0x4], 0x0
gef➤  p $rax
$1 = 0x67ab9472

Now the output of the first rand() :

───────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555555303 <shuffle+0021>   mov    DWORD PTR [rbp-0x4], eax
   0x555555555306 <shuffle+0024>   jmp    0x555555555362 <shuffle+128>
●  0x555555555308 <shuffle+0026>   call   0x5555555550f0 <rand@plt>
 → 0x55555555530d <shuffle+002b>   mov    edx, DWORD PTR [rbp-0x4]
   0x555555555310 <shuffle+002e>   lea    ecx, [rdx+0x1]
   0x555555555313 <shuffle+0031>   cdq    
   0x555555555314 <shuffle+0032>   idiv   ecx
   0x555555555316 <shuffle+0034>   mov    DWORD PTR [rbp-0xc], edx
   0x555555555319 <shuffle+0037>   mov    eax, DWORD PTR [rbp-0x4]
gef➤  p $rax
$2 = 0x6d692494

And of the second rand() :

───────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555555303 <shuffle+0021>   mov    DWORD PTR [rbp-0x4], eax
   0x555555555306 <shuffle+0024>   jmp    0x555555555362 <shuffle+128>
●  0x555555555308 <shuffle+0026>   call   0x5555555550f0 <rand@plt>
 → 0x55555555530d <shuffle+002b>   mov    edx, DWORD PTR [rbp-0x4]
   0x555555555310 <shuffle+002e>   lea    ecx, [rdx+0x1]
   0x555555555313 <shuffle+0031>   cdq    
   0x555555555314 <shuffle+0032>   idiv   ecx
   0x555555555316 <shuffle+0034>   mov    DWORD PTR [rbp-0xc], edx
   0x555555555319 <shuffle+0037>   mov    eax, DWORD PTR [rbp-0x4]
gef➤  p $rax
$3 = 0x1a174d95

Let’s see if we get the same outputs in Python with the same seed :

from ctypes import CDLL

if __name__ == "__main__":
    libc = CDLL("/usr/lib64/libc.so.6")
    libc.srand(0x67ab9472)
    r1, r2 = libc.rand(), libc.rand()

    print(hex(r1), hex(r2))
$ python3 solve.py
0x6d692494 0x1a174d95

This is working, we can predict the output of every rand().

Now we need to invert shuffle() by swapping back every byte to their right place one by one starting with the last pair of bytes that was swapped. That’s why we first need to determine the output of every rand() call. rand() being called once for every byte of message in shuffle() and shuffle() being called 22 times, rand() is called len(message)*22 in total. Let’s calculate every output of rand() :

from pwn import *
from ctypes import CDLL

def calc_rands(length:int, iterations:int, libc) -> list:
    rands = []
    for _ in range(length*iterations):
        rands.append(libc.rand())
    return rands

if __name__ == "__main__":
    p = process("./the-eye")

    libc = CDLL("/usr/lib64/libc.so.6")
    curr_time = libc.time(None)
    libc.srand(curr_time)
    
    message = p.recv()
    message = message.split(b"\n")[0]
    message = list(message.decode())

    rands = calc_rands(len(message), 22, libc)
    print(len(rands), rands[-1])

In this script I used pwntools to run the-eye, then used CDLL to run time() at the same time as the program to break it’s randomness, then saved every rand() output used by the program in rands :

$ echo "Testing this script" > msg.txt

$ python3 solve.py
[+] Starting local process './the-eye': pid 20734
[*] Process './the-eye' stopped with exit code 0 (pid 20734)
418 676159644

Now we only need to invert shuffle() using the generated rands :

from pwn import *
from ctypes import CDLL

def calc_rands(length:int, iterations:int, libc) -> list:
    rands = []
    for _ in range(length*iterations):
        rands.append(libc.rand())
    return rands

def shuffle_back(message:list, rands: list, libc) -> str:
    i = 0
    for r in reversed(rands):
        r = r % (i+1)
        message[i], message[r] = message[r], message[i]
        if (i == len(message)-1):
            i = 0
        else:
            i += 1
    return message

if __name__ == "__main__":
    p = process("./the-eye")

    libc = CDLL("/usr/lib64/libc.so.6")
    curr_time = libc.time(None)
    libc.srand(curr_time)
    
    message = p.recv()
    print(f"Received : {message}")
    message = message.split(b"\n")[0]
    message = list(message.decode())

    rands = calc_rands(len(message), 22, libc)
    message = shuffle_back(message, rands, libc)
    message = "".join(message)
    print(f"Decoded : {message}")

Which gives us :

$ echo "Testing this script" > msg.txt

$ python3 solve.py
[+] Starting local process './the-eye': pid 22050
[*] Process './the-eye' stopped with exit code 0 (pid 22050)
Received : b'tsTsn pticetshi igr\n'
Decoded : Testing this script

Exploit

Using this final script to connect back to the servers and decode the message we get :

$ python3 solve.py
[+] Opening connection to chall.lac.tf on port 31313: Done
Outer Wilds is an action-adventure video game set in a small planetary system in which the player character, an unnamed space explorer referred to as the Hatchling, explores and investigates its mysteries in a self-directed manner. Whenever the Hatchling dies, the game resets to the beginning; this happens regardless after 22 minutes of gameplay due to the sun going supernova. The player uses these repeated time loops to discover the secrets of the Nomai, an alien species that has left ruins scattered throughout the planetary system, including why the sun is exploding. A downloadable content expansion, Echoes of the Eye, adds additional locations and mysteries to the game. lactf{are_you_ready_to_learn_what_comes_next?}
[*] Closed connection to chall.lac.tf port 31313