Tags: janet rev
Rating:
# geoguesser (rev)
Writeup by: [xlr8or](https://ctftime.org/team/235001)
For this challenge we get an ELF binary `janet` (not stripped) and some blob `program.jimage`. The challenge description is also helpful in telling us how to run the file.
Running the file we note the following:
* we have 5 guesses
* no information is given regarding the numbers to guess
* we need to guess coordinates, and the format is `<decimal number>,<decimal number>`, not all number are accepted
Indeed it would be quite hard to guess the coordinate like this, so let's dive in.
First I got the feeling, that `janet` was not made specifically for the challenge, as some other binaries that are VMs, and yes there's the [janet language](https://janet-lang.org/), and the binary is the runtime for the language, so all the logic is hidden in `program.jimage`
From the CLI help we also know that the jimage file is a janet image file, and that janet could also work with source code (if we had it). The image contains some interesting strings, such as potential names of functions, and prompts that we see in the binary. The format of the image however is still not known.
There's some information about the [bytecode format](https://janet-lang.org/docs/abstract_machine.html), however this isn't enough to make a disassembler. I spent some time studying the janet runtime source code related to the image format and bytecode, however ultimately I have decided against writing a simple disassembler for this format.
After some further research I have stumbled upon an [article about compilation](https://janet.guide/compilation-and-imagination/), which I found quite helpful. Here I have learned about the `load-image` command, which can take the binary image we have and make some sense of it. Using `./janet` we can enter into a repl.
```
repl:1:> (def img (load-image (slurp "program.jimage")))
@{
compare-coord @{:doc "(compare-coord a b tolerance)\n\n" :source-map ("main.janet" 32 1) :value <function compare-coord>}
compare-float @{:doc "(compare-float a b tolerance)\n\n" :source-map ("main.janet" 29 1) :value <function compare-float>}
coordinate-peg @{:source-map ("main.janet" 8 1) :value { :float (number (some (+ :d (set ".-+")))) :main
(* :ss :float :sep :float :ss) :sep (* :ss "," :ss) :ss (any :s)}}
get-guess @{:doc "(get-guess)\n\n" :source-map ("main.janet" 21 1) :value <function get-guess>}
guessing-game @{:doc "(guessing-game answer)\n\n" :source-map ("main.janet" 36 1) :value <function guessing-game>}
init-rng @{:doc "(init-rng)\n\n" :source-map ("main.janet" 5 1) :value <function init-rng>}
main @{:doc "(main &)\n\n" :source-map ("main.janet" 54 1) :value <function main>}
parse-coord @{:doc "(parse-coord s)\n\n" :source-map ("main.janet" 15 1) :value <function parse-coord>}
precision @{:source-map ("main.janet" 1 1) :value 0.0001}
print-flag @{:doc "(print-flag)\n\n" :source-map ("main.janet" 46 1) :value <function print-flag>}
random-float @{:doc "(random-float min max)\n\n" :source-map ("main.janet" 51 1) :value <function random-float>}
rng @{:ref @[nil] :source-map ("main.janet" 3 1)}
:current-file "main.janet"
:macro-lints @[]
:source "main.janet"
}
```
What you see with `@{}` is a table in janet, and keys are usually prefixed with `:`, however for most functions it is not the case (I'm not sure why, but it's not too important).
Here we note several useful pieces of information
* we have a bunch of function names and signatures
* we have the acutal function (whatever that may be)
* we have some constants, such as `precision` and `coordinate-peg` looks like some regex trickery.
I think it is important to mention the [API documentation](https://janet-lang.org/api/index.html) at this point, which lists a bunch of builtin janet functions/commands that we are going to use, or are used in the challenge binary.
Let's see now where we could attack this challenge:
1. We could try to break input parsing (seems more like a pwn challenge, but still a possibility)
2. We could try to defeat the random generator
During the contest I have looked a bit into 1, however nothing seemed out of the ordinary, so I went with 2, since I thought it to be a more promising approach.
The next important milestone in my janet journey was the ability to disassemble functions finally. The API docs I have linked above mention a `disasm` function, that can take a function and return some information about it. One small issue was that I couldn't index the table by the name of the function for some reason (I didn't manage to figure out why this is). Instead you will see some hacky workaround, sorry in advance
```
repl:3:> (def init-rng (get (get (values img) 4) :value))
<function init-rng>
repl:4:> (disasm init-rng)
{:arity 0 :bytecode @[ (lds 0) (ldc 2 0) (call 1 2) (push 1) (ldc 3 1) (call 2 3) (ldc 1 2) (puti 1 2 0) (ldc 1 2) (geti 1 1 0) (ret 1)] :constants @[<cfunction os/time> <cfunction math/rng> @[nil]] :defs @[] :environments @[] :max-arity 0 :min-arity 0 :name "init-rng" :slotcount 4 :source "main.janet" :sourcemap @[ (5 1) (6 22) (6 22) (6 12) (6 12) (6 12) (6 12) (6 12) (6 3) (6 3) (6 3)] :structarg false :symbolmap @[(0 11 0 init-rng)] :vararg false}
```
First I get all the values of the table, which gives an array, and then I get the entry at the 4th index, which is the `init-rng` entry. The result sofar is another table, where the `:value` key containst the function itself, so I retrieve it. Now the result if a function and I save it in `init-rng`. Now we can disassemble the function and that will give the rather concise output above, let me clean it up a bit.
```
Constants: [<cfunction os/time> <cfunction math/rng> @[nil]]
lds 0 ; d[0] = self
ldc 2 0 ; d[2] = const[0]
call 1 2 ; d[1] = const[2]()
push 1 ; argstack.push(d[1])
ldc 3 1 ; d[3] = const[1]
call 2 3 ; d[2] = d[3](d[1]) -- d[1] filled from argstack
ldc 1 2 ; d[1] = d[2]
puti 1 2 0 ; d[1][0] = d[2]
ldc 1 2 ; d[1] = const[2]
geti 1 1 0 ; d[1] = d[1][0]
ret 1 ; return d[1]
```
Essentially this will call `os/time`, then call `math/rng` with its result, then finally overwrite `math/rng` with the result of the previous call. In human terms this seeds the rng with the result of `os/time`. According to the documentation `os/time` return the unix epoch time.
This is important! We know that the RNG will be seeded with a value we can potentially guess, since the precision is only seconds. We are allowed 5 guesses, and unix epoch time is not affected by time zones, so the seed which is used will be closed the the unix time we can get at the time of connecting to the challenge remote.
Now we need a bit more exploration, before we are ready to exploit the system. Namely we need to replicate the random generation. During the contest I have disassembled this as well, however a faster solution would have been to simply reuse the existing function while controlling the seed of the RNG.
Disassembly for `random-float`
```
Constants: [@[nil] <cfunction math/rng-uniform>]
lds 2 ; ds[2] = self
ldc 3 0 ; ds[3] = const[0]
geti 3 3 0 ; ds[3] = ds[3][0]
push 3 ; argstack.push(ds[3])
ldc 4 1 ; ds[4] = const[1]
call 3 4 ; ds[3] = ds[4](ds[3]) -- filled from argstack
sub 4 1 0 ; ds[4] = $param1 - $param0 -- $param1 is in ds[1] and $param0 is in ds[0]
mul 5 3 4 ; ds[5] = ds[3] * ds[4]
add 3 0 5 ; ds[3] = $param0 + ds[5]
ret 3 ; return ds[3]
```
This will first generate a random number [0, 1), multiply it by `high - low`, then add `low` to the result and return it. Essentially giving a random floating point number between `low` and `high`, where `low = $param0` and `high = $param1`.
Next let's take a look at the start of the `main` function:
```
Constants: ["Welcome to geoguesser!" <cfunction print> <function init-rng> <function random-float> <function guessing-game> <function print-flag> "You lose!" "The answer was: "]
lds 0 ; ds[0] = self
ldc 1 0 ; ds[1] = const[0]
push 1 ; argstack.push(ds[1])
ldc 2 1 ; ds[1] = const[1]
call 1 2 ; ds[1] = ds[2](ds[1]) -- filled from argstack
ldc 3 2 ; ds[3] = ds[2]
call 2 3 ; ds[2] = ds[3]()
ldi 3 -90 ; ds[3] = -90
ldi 4 90 ; ds[4] = 90
push2 3 4 ; argstack.push(ds[4]) && argstack.push(ds[3])
ldc 4 3 ; ds[4] = const[3]
call 3 4 ; ds[3] = ds[3](ds[3], ds[4]) -- filled from argstack
ldi 4 -180 ; ds[4] = -180
ldi 5 180 ; ds[4] = 180
push2 4 5 ; argstack.push(ds[5]) && argstack.push(ds[4])
ldc 5 3 ; ds[5] = const[3]
call 4 5 ; ds[4] = ds[5](ds[4], ds[5]) -- filled from argstack
...
```
We see that first it will print the welcome message, and then call `init-rng` (`const[2]`), then call `random-float` (`const[3]`) twice, the first time (-90, 90) is passed as arguments, the second time (-180, 180) is passed.
Now we know everything we need to exploit this game.
1. We get our unix epoch time
2. We connect to the remote
3. For a window around the current time, we generate 2 random numbers first (-90, 90), then (-180, 180) using the time as a seed to the rng, We will use `janet` for this in case the random generation differs from how python does it
4. Hope we get the flag
The python script below will carry out the exploitation:
```python
from pwn import *
import time
def get_random_coords(seed):
command = f'(def myrng (math/rng {seed})) (def rnglat (+ (* (math/rng-uniform myrng) (- 90 -90)) -90)) (def rnglong (+ (* (math/rng-uniform myrng) (- 180 -180)) -180)) (print rnglat) (print rnglong)'
p = process(['./janet', '-e', command])
lat = p.recvline().strip()
long = p.recvline().strip()
p.kill()
print('local: ', lat, long)
return lat, long
t1 = int(time.time())
rem = remote('geoguesser.chal.uiuc.tf', 1337)
for i in range(0, 5):
curt = t1 + i
lat, long = get_random_coords(curt)
rem.sendlineafter(b'Where am I?', lat + b',' + long)
content = rem.recvline()
print('got', content)
if b'win!' in content: rem.interactive()
input()
print('done')
```
* `get_random_coords` will generate the 2 coordinates the game creates in janet, using the exact way the game does it.
The rest of the script will try the next 4 values from the timestamp we store before connecting to the remote, and switch to interactive if a win is detected.
```
➜ geoguesser python solve.py
[+] Opening connection to geoguesser.chal.uiuc.tf on port 1337: Done
[+] Starting local process './janet': pid 195854
[*] Stopped process './janet' (pid 195854)
local: b'87.5874' b'-168.768'
got b' Nope. You have 4 guesses left.\n'
[+] Starting local process './janet': pid 195856
[*] Stopped process './janet' (pid 195856)
local: b'2.25373' b'-139.84'
got b' You win!\n'
[*] Switching to interactive mode
The flag is: uiuctf{i_se3_y0uv3_f0und_7h3_t1m3_t0_r3v_th15_b333b674c1365966}
[*] Got EOF while reading in interactive
```