In this crypto challenge we need to decypher an image encrypted in a wrong way.

The challenge

We have two things :

  • The source code they use to encrypt the flag
  • An access in tcp to an instance to run the code and get as many encrypted flag we want

The source code

Here is the source code (a little bit modified) :

import base64
import os


FLAG = base64.b85encode(open("flag.bmp", "rb").read()).decode()

CHARSET = base64._b85alphabet.decode()
N = len(CHARSET)


def generate_key(length):
    random_stream = os.urandom(length)
    return "".join(CHARSET[x % N] for x in random_stream)


def encrypt(plaintext, key):
    ciphertext = ""
    for i in range(len(plaintext)):
        ciphertext += CHARSET[(CHARSET.find(plaintext[i]) + CHARSET.find(key[i])) % N]
    return ciphertext

key = generate_key(len(FLAG))
print(encrypt(FLAG, key))

Few things about it :

  • The flag is an image
  • Every time we ask a flag a new key is generated with the same length as the flag
  • The flag we get is in base85
  • They use os.urandom which do not have known vulnerability (we will say it is perfect)

Exploitation

Like I say os.urandom is perfect so the vulnerability is somewhere else.

The vulnerability is in the fact they use base85. Every byte given by os.urandom is then taken modulo N the length of base85 alphabet so modulo 85. And by chance 255 = 3*85 which mean if I take a random byte I have more chance to get a 0 modulo 85 than an other number (because 0, 85, 170 and 255 will give me a 0 modulo 85 so 4 diffferent bytes wherease the other number will only have 3 different bytes).

So we just need to get enough encrypted flags and then for a given char take the one in majority over all the different flags because it would have been statisticly encrypted with a 0. Of course, as it’s only for statistics, we really need to recover a lot of flags or we will get an error.

I recover 50000 flags which take me about 30 minutes.

Here is the code I use :

from pwn import *

SERVER_HOTE = "instances.challenge-ecw.fr"
PORT = 38648

CHARSET = base64._b85alphabet.decode()
N = len(CHARSET)

def get_flag(nb):
    con = remote(SERVER_HOTE, PORT)
    d = con.recvlines(7)
    print(d)
    d = con.recv()
    print(d)
    t = []
    for i in range(nb):
        print(i)
        con.sendline(bytes("1", "utf-8"))
        d = con.recvline()[:-1]
        t.append(d)
        con.recv()
    con.close()
    return t

def find_max(t):
    res = []
    for i in range(len(t[0])):
        d = dict()
        for j in range(len(t)):
            a = t[j][i]
            if a in d:
                d[a] += 1
            else:
                d[a] = 1
        m = 0
        r = None
        for z in d:
            if d[z] > m:
                m = d[z]
                r = z
        res.append(r)
    return res

def to_string(r):
    s = ""
    for i in r:
        s += chr(i)
    return s

def to_write(e:str):
    b = base64.b85decode(e.encode())
    with open ("flag.bmp", "wb") as f:
        f.write(b)            
    
def main():
    t = get_flag(50000)
    r = find_max(t)
    s = to_string(r)
    write_to_txt(t)
    to_write(s)
    
if __name__ == "__main__":
    main()

Flag

Finaly we get this image :

And so the flag is ECW{b85_modulo_bias_!!}