The challenge starts here: https://france-cybersecurity-challenge.fr/

We need to find the flag on the website.

The source code gave:

Steganography

interesting links for this part:

The png image is interesting:

steganography

Do you see the small white dots on the top of the letters “CE CYBERSECU”?

After editing with Gimp we got: steganography_green

Now let’s follow the checklist:

  1. check type

    file stegano.png
    
    stegano.png: PNG image data, 2048 x 1968, 8-bit/color RGB, non-interlaced
    

    Nothing interesting here

  2. check strings

    strings -n 17 -t x  stegano.png
    
    43 https://www.youtube.com/watch?v=dQw4w9WgXcQ
    

    oh oh !

    Or… No. Nothing interesting…

  3. check exif. Nothing interesting in the exif

  4. binwalk

    binwalk -Me stegano.png
    

    We got 2 files: an empty 7B and 7B.zlib. These are png data. Nothing special here.

Now, let’s focus on the image.

With a bit of manual work, we can decode the content: steganography_green

So next step is here: https://france-cybersecurity-challenge.fr/bpbcmrplfgzmztgggpbc. On this page, we got an executable for TI-83+.

TI-83+ program analysis

file fcsc.8xp
fcsc.8xp: TI-83+ Graphing Calculator (assembly program)

Wow! This brings back memories!

Documentation will be useful: http://lpg.ticalc.org/prj_tilem/doc/user_manual.pdf. Also a small Z-80 tuto

Interesting stuff found with TilEm:

  • in memory, program starts at 9D76
  • the conditional jump that lead to the piece of code displaying the flag is at address 9D95
  • when hacking some CALL to execute this code we get:

So the flag is dynamic….

Guess it’s time to go with more powerful tools like Ghidra to understand what’s going on.

8xp analysis

After some Google search, we found that a 8xp file is structured like this:

Packet
|
| Header
| |   0         8       **TI83F*
| |   8         3       $1A $0A $00
| |   11        42      Comment - padded with zeros or spaces.
| |   53        2       Length of data section.  Should be equal to filesize - 57
| End Header
|
| Data
| |   55        2       Always has a value of $0B or $0D? (prefer $0B?)
| |   57        2       Length of Variable Data Section
| |   59        1       Variable type ID ($05 for programs)
| |   60        8       Variable name (padded with zeros)
| |   68        1       Version (??), usually $00.  (Present if byte 55 == $0D)
| |   69        1       Archive flag.  $80 archived, $00 if not. (Present if byte 55 == $0D)
| |   70        2       Length of Variable Data Section (repeat of bytes @ 57)
| |
| | Variable Data
| | | 72        2       Length of Actual Data - 2
| | | 74        n       Actual Data
| | End Variable Data
| |
| End Data
|
|    74 + n    2       Checksum (sum of Data section)
|
End Packet

with a small python script we can extract the code.

import struct

xp8_file = "~/workspace/CTF/FCSC_2022/fcsc.8xp"

with open(xp8_file, "rb") as f:
    # Header
    print("Header:", f.read(11))
    print("Comment:", f.read(42))
    data_section_length = f.read(2)
    print("Length of the Data Section", data_section_length, int.from_bytes(data_section_length, "little")+57)

    # Data
    print("Random Stuff", f.read(1).hex(), f.read(1).hex())
    print("Length of Variable Data Section", int.from_bytes(f.read(2), "little"))
    print("Variable type ID", f.read(1).hex())
    print("Variable name", f.read(8))
    print("Version", f.read(1))
    print("Archive flag", f.read(1))
    print("Length of Variable Data Section", int.from_bytes(f.read(2), "little"))
    data_length = int.from_bytes(f.read(2), "little")
    print("Length of Length of Actual Data - 2:", data_length)
    f.read(2) # remove 2 useless bytes
    data = f.read(data_length)

edited_xp8_file = "~/workspace/CTF/FCSC_2022/fcsc_edited.8xp"
with open(edited_xp8_file, "wb") as f:
    f.write(data)

Now we can load it in Ghidra, specifying that this is Z80 asm with a Base Address of 9D95.

The analysis took… a lot of time. The important parts are:

  • functions at address 9DE0 and 9DA2 are validating the 32 input characters
  • for each correct char, the function at address 9F78 is called
  • Setting a breakpoint for instance at address 9F78 and looking at the first address in the stack will be a way to know which characters are correct.

After testing inputs full of 0, 1, … 9, A, B, … F we can find the flag:

FCSC{4B8CB9E0ADB7F2B3BA4E6CB7156F1243}

Other things learned