HITCON 2016: Let's Decrypt (Crypto, 100pts)

Description:
It’s free, automated, and open.
nc 52.69.125.71 4443
Flag:
hitcon{R4nd0m IV plz XD}

This problem illustrates how easy it is to break AES encryptions with fixed initialization vectors (IV). Upon connecting to the server, they give you the option to pull down the source code, which resulted in:

#!/usr/bin/env ruby
require 'openssl'  
require 'timeout'

$stdout.sync = true
Dir.chdir(File.dirname(__FILE__))

class String  
  def enhex
    self.unpack('H*')[0]
  end

  def dehex
    [self].pack('H*')
  end
end

flag = IO.read('flag')  
KEY = IV = flag[/hitcon\{(.*)\}/, 1]  
fail unless KEY.size == 16

def aes(s, mode)  
  cipher = OpenSSL::Cipher::AES128.new(:CBC)
  cipher.send(mode)
  cipher.key = KEY
  cipher.iv = IV
  cipher.update(s) + cipher.final
end

def encrypt(s); aes(s, :encrypt) end  
def decrypt(s); aes(s, :decrypt) end

m = 'The quick brown fox jumps over the lazy dog'  
c = encrypt(m)  
fail unless c.enhex == '4a5b8d0034e5469c071b60000ca134d9e04f07e4dcd6cf096b47ba48b357814e4a89ef1cfad33e1dd28b892ba7233285'  
fail unless c.enhex.dehex == c  
fail unless decrypt(c) == m

begin  
  Timeout::timeout(30) do
    puts '1) Show me the source'
    puts "2) Let's decrypt"
    cmd = gets.to_i
    case cmd
    when 1
      puts IO.read(__FILE__)
    when 2
      c = gets.chomp.dehex
      m = decrypt(c)
      puts m.enhex
    else
      puts '...meow?'
    end
  end
rescue Timeout::Error  
  puts 'Timeout ._./'
end  

Important lines to note are:
KEY = IV = flag[/hitcon\{(.*)\}/, 1] m = 'The quick brown fox jumps over the lazy dog' and
fail unless c.enhex == '4a5b8d0034e5469c071b60000ca134d9e04f07e4dcd6cf096b47ba48b357814e4a89ef1cfad33e1dd28b892ba7233285'

They basically made you life super easy by not having to do an oracle attack. All you have to do is get the server to decrypt a block you know the decryption of for the first block so you can recover the IV (and therefore the key and the flag). Luckily, we know that the last block of 4a5b8d0034e5469c071b60000ca134d9e04f07e4dcd6cf096b47ba48b357814e4a89ef1cfad33e1dd28b892ba7233285 is padded correctly. We also can know the decryption of the middle block (not the plain text) since CBC Xor's the plaintext with the previous cipher text then encrypts that. Thus, if we Xor hex(m[16:32]) with c[0:32], we get the decryption of the middle block before it is Xor'd with the previous cipher text to get the plain text.

Now we have all the tools to find the IV. Look what we can do if we send the server the string c[32:64] + c[32:64] + c[64:96]. Here is the mathematical analysis of it:

plaintext_block1 = Decrypt(ciphertext_block1) ^ IV  
= (Decrypt(ciphertext_block2)) ^ IV
= (ciphertext_block1 ^ plaintext_block2) ^ IV
= c[32:64] ^ plaintext_block2 ^ IV

IV = plaintext_block1 ^ c[32:64] ^ plaintext_block2  

I wrote a script that does exactly this:

import socket  
HOST = "52.69.125.71"  
PORT = 4443

c = "4a5b8d0034e5469c071b60000ca134d9e04f07e4dcd6cf096b47ba48b357814e4a89ef1cfad33e1dd28b892ba7233285"  
c1 = c[0:32]  
c2 = c[32:64]  
c3 = c[64:96]

print

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
s.connect((HOST, PORT))

s.recv(1024)  
s.recv(1024)  
s.sendall("2\n")

s.sendall(c2 + c2 + c3 + "\n")  
malicious = s.recv(1024)

flag = ""  
for i in range(0, 32, 2):  
    flag += chr(int(malicious[i:i+2], 16) ^ int(malicious[32+i:32+i+2], 16) ^ int(c2[i:i+2],16))

print flag  
#R4nd0m IV plz XD

This script outputs the flag R4nd0m IV plz XD.