babypad (crypto154)

We heard this kind of enription is super securr, so we’ll just give you the flag encripted!

image

nc hitme.tasteless.eu 10401

$ sha1sum chall.c

d64fc2e2f979b693696efe1762e18153df1b6170 chall.c

Author: plonk

chall.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

int main() {
    char *plaintext = NULL;
    char *one_time_pad = NULL;
    char *ciphertext = NULL;
    size_t flag_length = 0;
    FILE *flag = fopen("flag.txt", "r");
    FILE *urandom = fopen("/dev/urandom", "r");
    assert(flag && urandom);

    /*
     * Get flag length, and allocate memory for the plaintext,
     * one-time pad and ciphertext.
     */
    fseek(flag, 0, SEEK_END);
    flag_length = ftell(flag);
    rewind(flag);

    plaintext = malloc(flag_length + 1);
    one_time_pad = malloc(flag_length + 1);
    ciphertext = malloc(flag_length + 1);
    assert(plaintext && one_time_pad && ciphertext);

    /* Read the plaintext and the one-time-pad */
    fread(plaintext, flag_length, 1, flag);
    fread(one_time_pad, flag_length, 1, urandom);
    plaintext[flag_length] = '\0';
    one_time_pad[flag_length] = '\0';

    /* Make sure that the one-time-pad isn't too short. */
    assert(strlen(plaintext) == strlen(one_time_pad));

    for (int i = 0; i < flag_length; i++) {
        ciphertext[i] = plaintext[i] ^ one_time_pad[i];
    }

    fwrite(ciphertext, flag_length, 1, stdout);
    return 0;
}

nc hitme.tasteless.eu 10401

ctfs/tasteless19/cry
▶ nc hitme.tasteless.eu 10401

h�ѐ�i�K�K�<d�$S�*0E�K󹬨͟%
ctfs/tasteless19/cry
▶ nc hitme.tasteless.eu 10401
	>�LJJh^{����6���?�Yă{�[p���5�Ma�%
ctfs/tasteless19/cry
▶ nc hitme.tasteless.eu 10401
�x��co;�#�{�OÂC�\�"i��j͑>��0ǂg�%

In summary, the challenge program reads the flag from a file, then reads the same number of random bytes from /dev/urandom as a keystream, and performs byte-by-byte xor encryption on the flag with the randomly generated keystream.

The result can be obtained by connecting to the service at hitme.tasteless.eu:10401. Clearly, everytime we connect to the service, the output will be different because it is based on bytes randomly generated.

Problem

In theory, this uses a one-time pad which means the plaintext (flag) cannot be obtained without knowing all the bytes used to encrypt it. Also /dev/urandom is cryptographically secure, meaning it is impossible to predict the random numbers generated from it.

However, there is a severe design flaw in the following line

    /* Make sure that the one-time-pad isn't too short. */
    assert(strlen(plaintext) == strlen(one_time_pad));

It looks good but what it does is also ensuring that there are no null bytes in the keystream.

This means that during encryption, the flag bytes can be xored with any random byte except \x00, which also means for each byte, the encrypted output can be any byte except for itself.

Solution

Now it should be clear that the solution is to get as many encrypted flags from the service as possible, and for each position in the flag, eliminate the bytes that appeared in the encrypted flag until there is one character left.

from pwn import *
from z3 import *
from socket import gaierror
import itertools

context.log_level = 'error'

flag = [[j for j in range(0x100)] for i in range(37)]

while not all([len(a) == 1 for a in flag]):
    try:
        r = remote('hitme.tasteless.eu', 10401)
        c = r.recv()
        
        for i in range(37):
            if ord(c[i]) in flag[i]:
                flag[i].remove(ord(c[i]))
    except EOFError:
        continue
    except gaierror:
        continue

    print([len(a) for a in flag])

print(flag)


# >>> x = [[116], [99], [116], [102], [123], [112], [49], [122], [95], [117], [115], [51], [58], [52], [108], [108], [45], [116], [51], [104], [95], [98], [121], [55], [101], [53], [62], [48], [110], [51], [95], [116], [105], [109], [51], [125]]
# >>> ''.join(map(chr, [a[0] for a in x]))
# 'tctf{p1z_us3:4ll-t3h_by7e5>0n3_tim3}'