/den/face0xff/writeups
  • Introduction
  • 2023
    • FCSC 2023
      • Hola Amigo (reverse)
      • Picasso (reverse)
  • 2022
    • Root-Me 10K CTF
      • chef's kiss
    • FCSC 2022
      • httpd (pwn)
      • Hyper Packer (reverse)
      • Khal Hash (crypto)
    • Hackday Qualifications 2022
      • Cubik'cipher
    • DiceCTF 2022
      • cable management
  • 2021
    • Aero CTF 2021
      • Not received prize
    • FCSC 2021
      • Shared Notes (web, 500)
      • BattleChip (misc, 495)
    • UIUCTF 2021
      • phpfuck_fixed
  • 2020
    • FCSC Prequals 2020
      • Keykoolol (reverse, 500)
      • Macaron (crypto, 200)
      • Merry (crypto, 500)
      • SSEcret (reverse, 500)
      • Why not a sandbox? (pwn, 500)
    • European Cyber Week CTF Qualifiers 2020
      • Antirdroid
      • Windtalkers
    • DefCamp CTF 2020
      • dumb-discord, spy agency, cross me, syntax check
    • ångstromCTF 2020
      • RSA-OTP
  • 2019
    • ECW Quals 2019
      • S3cr37 4g3nt
    • X-MAS CTF 2019
      • FUNction Plotter
      • Emu 2.0
    • Square CTF 2019
      • Go Cipher
    • Byte Bandits CTF 2019
      • babycrypto
    • Securinets Prequals 2019
      • Beginner's Luck
    • STEM CTF 2019
      • QvR Code
      • REbase
    • Pragyan CTF 2019
      • Decode This
      • Save Earth
      • Super Secure Vault
    • CTFZone 2019
      • Agents
Powered by GitBook
On this page
  • Crypto, 1000 points
  • Description of the algorithm
  • Exploitation of the algorithm
  • Conclusion

Was this helpful?

  1. 2019
  2. Square CTF 2019

Go Cipher

Crypto, 1000 points

In this challenge, we were given a Go source file which allows to encrypt or decrypt data using a 24-byte key. Our goal was to decrypt flag.txt.enc without knowing the key.

Description of the algorithm

The interesting part of the code is the encrypt function:

func encrypt(plaintext []byte, key []byte) string {
  x := uint64(binary.LittleEndian.Uint64(key[0:]))
  y := uint64(binary.LittleEndian.Uint64(key[8:]))
  z := uint64(binary.LittleEndian.Uint64(key[16:]))

  keyid := md5.Sum(key)
  r := keyid[:]
  for _, e := range plaintext {
    t := (e - byte(x)) ^ byte(y) ^ byte(z)
    r = append(r, t)
    x = bits.RotateLeft64(x, -1)
    y = bits.RotateLeft64(y, 1)
    z = bits.RotateLeft64(z, 1)
  }
  return hex.EncodeToString(r)
}

The idea is that our 24-byte key is split into 3 chunks of 8 bytes each, and then converted into 64-bit integers x, y and z.

The key is then hashed into md5 and the ciphertext starts with this hash (16 bytes). As far as my understanding went, the sole purpose of this md5 is to ensure a key is correct before trying to decrypt, and it isn't really exploitable to crack the key.

The encryption algorithm is quite simple; if e is a byte of the plaintext, then it will be encrypted into (e - byte(x)) ^ byte(y) ^ byte(z), where byte(a) denotes the 8 least significant bits of a (in other words, a & 0xFF). After each iteration, x is rotated 1 bit to the right, and y, z are rotated 1 bit to the left.

Exploitation of the algorithm

Right off the bat, we can notice it is useless to look for two separate variables y and z. Indeed, their roles are perfectly symmetric and the values of x, y and z are never interchanged throughout the encryption. From an attacker's point of view, it is thus equivalent to let u = y ^ z and t = (e - byte(x)) ^ byte(u), u being rotated 1 bit to the left each iteration.

My idea was that we only have to brute-force the first byte of x and u (65536 possibilities maximum), and for each valid possibility, because of the 1-bit rotation mechanism, each iteration that follows we only have to find out whether the next bit for x and u is 0 or 1, which leaves 4 possibilities maximum. We can then explore the tree describing every possible plaintext with a recursive algorithm.

But how do we narrow down the possibilities? Well, since the encrypted flag has a pretty small size (47 bytes), we expect it to be normal text, so all we have to do is check if the potential decrypted byte at each iteration is readable ASCII. We also know that the flag will look like flag-[hex chars], so we can look out for the string "flag-" in each tree path.

Let's sum up the steps of the attack:

  • Choose a set abc of characters you expect the plaintext to be made of

  • Skip the 16 first bytes of the encrypted flag flag

  • Start with an "empty" x and u, and an empty plaintext out

  • Start by brute-forcing the values (p, q) of the lower byte of x and u:

    • If (flag[0] ^ q) + p) is in abc, go deeper and append this value to out

  • Then brute-force the next bit of x and y:

    • Compute the new value (p, q) of the lower byte of x and u after the bit rotation

    • If (flag[0] ^ q) + p) is in abc, go deeper and append this value to out

  • If a path reaches the end (length of the ciphertext), check if it has "flag-" in it and display it!

Here is a Python implementation of the attack:

flag = "9e108b46c49f48b25591375a0ed7716a952a25e0b1d1242e4587f9e9c119e3b7f4d3d063b9a5cdf298e2b2a4a9b42835febde85f690ca6997100351ebdb17b"
flag = bytes.fromhex(flag)[16:]

printable = [ord(x) for x in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJLMNOPQRSTUVWXYZ- 0123456789,.?!\n"]

def rec(key_x, key_y, out):
    i = len(out)
    if i == len(flag):
        if b"flag-" in out:
            print(out)
    elif i == 0:
        for x in range(256):
            for y in range(256):
                if ((flag[i] ^ y) + x) & 0xff in printable:
                    rec(x, y, out + bytes([((flag[i] ^ y) + x) & 0xff]))
    else:
        for p in range(2):
            for q in range(2):
                x = (p << 7) | (key_x >> 1)
                y = ((key_y << 1) & 0xff) | q
                if ((flag[i] ^ y) + x) & 0xff in printable:
                    rec(x, y, out + bytes([((flag[i] ^ y) + x) & 0xff]))

rec(0, 0, b"")

It takes only a few seconds for the flag to show up:

[...]
b'Yes, you did it! flag-742CF8ED6A2BF55807C.5ADta'
b'Yes, you did it! flag-742CF8ED6A2BF55807B0194T!'
b'Yes, you did it! flag-742CF8ED6A2BF55807B0194T '
b'Yes, you did it! flag-742CF8ED6A2BF55807B14719\n'
b'Yes, you did it! flag-742CF8ED6A2BF55807B135-Az'
b'Yes, you did it! flag-742CF8ED6A3DJ-EWq2u3pvcxF'
[...]

Conclusion

Go Cipher was pretty fun and simple, and I really enjoy those kinds of crypto tasks. This is also the first time I'm writing up for my new team SHRECS! We ended up 37th on the Square CTF 2019, which is nice but I wish we could have scored more, if we were less busy.

Enjoy!

PreviousSquare CTF 2019NextByte Bandits CTF 2019

Last updated 5 years ago

Was this helpful?