importbinasciifrombase64importb64encode,b64decodefromCrypto.CipherimportAESfromCrypto.Util.Paddingimportpad,unpadfromosimporturandomfromrandomimportseed,randintBLOCK_SIZE=16defrand_block(key_seed=urandom(1)):seed(key_seed)returnbytes([randint(0,255)for_inrange(BLOCK_SIZE)])defencrypt(plaintext,seed_bytes):ciphertext=pad(b64decode(plaintext),BLOCK_SIZE)seed_bytes=b64decode(seed_bytes)assertlen(seed_bytes)>=8forseedinseed_bytes:ciphertext=AES.new(rand_block(seed),AES.MODE_ECB).encrypt(ciphertext)returnb64encode(ciphertext)defdecrypt(ciphertext,seed_bytes):plaintext=b64decode(ciphertext"rgbCTF 2020 Crypto - N-AES"seed_bytes=b64decode(seed_bytes)forbyteinreversed(seed_bytes):plaintext=AES.new(rand_block(byte),AES.MODE_ECB).decrypt(plaintext)returnb64encode(unpad(plaintext,BLOCK_SIZE))defgen_chall(text):text=pad(text,BLOCK_SIZE)foriinrange(128):text=AES.new(rand_block(),AES.MODE_ECB).encrypt(text)returnb64encode(text)defmain():challenge=b64encode(urandom(64))print(gen_chall(challenge).decode())whileTrue:print("[1] Encrypt")print("[2] Decrypt")print("[3] Solve challenge")print("[4] Give up")command=input("> ")try:ifcommand=='1':text=input("Enter text to encrypt, in base64: ")seed_bytes=input("Enter key, in base64: ")print(encrypt(text,seed_bytes))elifcommand=='2':text=input("Enter text to decrypt, in base64: ")seed_bytes=input("Enter key, in base64: ")print(decrypt(text,seed_bytes))elifcommand=='3':answer=input("Enter the decrypted challenge, in base64: ")ifb64decode(answer)==challenge:print("Correct!")print("Here's your flag:")withopen("flag",'r')asf:print(f.read())else:print("Incorrect!")breakelifcommand=='4':breakelse:print("Invalid command!")exceptbinascii.Error:print("Base64 error!")exceptException:print("Error!")print("Bye!")if__name__=='__main__':main()
On netcatting, we get get a base64 encoded encryption of a base64 encoded random string of 64 bytes.
This seems quite tricky, since rand_block will be presenting some random key and gen_chall is encrypting with some random key 128 times! right? WRONG, There are some few caveats which we might exploit ;)
Since no key_seed is specified in the gen_chall call to rand_block, it should be taking key_seed to be urandom(1) which is simply one byte :)
More importantly, once it gets called, key_seed is fixed! So all the random blocks would essentially be the same! One may test it out.
So all that needs to be done is find out that random byte with which seed was initialised, and we will know the key, just decrypt our way out of the flag.
Solution
Python36 lines / static highlight
frompwnimportremotefrombase64importb64encode,b64decodefromCrypto.CipherimportAESfromCrypto.Util.Paddingimportpad,unpadfromrandomimportseed,randintimportreHOST,PORT="challenge.rgbsec.xyz",34567REM=remote(HOST,PORT)CHALL=b64decode(REM.recvline().strip())defrand_block(byte):"""random block for given seed byte"""seed(byte)returnbytes([randint(0,255)for_inrange(16)])REM.recvuntil(b'\n>')defdec_serv(ciphertext,seed_bytes):"""Requests decryption from the server"""REM.sendline(b'2')REM.sendline(b64encode(ciphertext))REM.sendline(b64encode(seed_bytes))data=REM.recvuntil(b'\n>')ifb'Error'notindata:decd=re.search(b'b\'([a-zA-Z0-9\+/]+)\'',data)[1]returnb64decode(decd)foriinrange(256):decryption=dec_serv(CHALL,bytes([i]*128))ifdecryption:print(decryption)breakREM.sendline(b'3')REM.sendline(b64encode(decrypt(CHALL)))print(REM.recvregex(b'rgbCTF{.*}').decode())
WAIT! THAT WONT WORK!!
Tbh, I expected that to work but it didnt! Why?
Because server uses this decryption routine
Still cant spot it out?
All the devil is in rand_block(byte). How? Because when byte objects are iterated upon, all the individual bytes are returned as int.
Python4 lines / static highlight
foriinb'a':print(i,type(i))#97 <class 'int'>
Hmm, very interesting. But how does that make a difference?
Because rand_block(i) and rand_block(byte([i]) are completly different for an int i! Why?
Because internally seed(key_seed) is used to initialize, and seed(byte([i])) and seed(i) are different! WTF!!
This implies the server would not never be able to decrypt using its own decryption routine!
To fix this, all we need to do is to write our own!
We know the decryption is correct just by looking at correct padding, since len(b64encode(64 random bytes)) = 64*4/3 = 85 and we have a ciphertext of len 96.
frompwnimportremotefrombase64importb64encode,b64decodefromCrypto.CipherimportAESfromCrypto.Util.Paddingimportpad,unpadfromrandomimportseed,randintimportreHOST,PORT="challenge.rgbsec.xyz",34567REM=remote(HOST,PORT)CHALL=b64decode(REM.recvline().strip())defrand_block(byte):"""random block for given seed byte"""seed(byte)returnbytes([randint(0,255)for_inrange(16)])REM.recvuntil(b'\n>')defdecrypt(ct):ct_orig=ctforiinrange(256):ct=ct_origfor_inrange(128):ct=AES.new(rand_block(bytes([i])),1).decrypt(ct)try:returnunpad(ct,16)except:continueREM.sendline(b'3')REM.sendline(b64encode(decrypt(CHALL)))print(REM.recvregex(b'rgbCTF{.*}').decode())