CTF | wargame

hackcenter / Broken Encryption 1 (CTR블록 암호, Crime Attack - 암호문 압축 길이에 따른 공격 기법)

nopdata 2017. 3. 7. 21:13

keyword : ctr 블록 암호, 파이썬 소스코드 분석



We found this service running on enigma2017.hackcenter.com:32762. Unfortunately, the sensitive information such as the encryption key and the flag have all been deleted. Our cryptographic spies tell us it is still insecure. Can you steal the flag?

이전 문제와 유사한 문제이다. 단, 블록 사용 방식이 CTR이라는 것이다. 실제 문제에서 CTR이 적용된 것은 처음 보는 듯 하다.

[ Counter mode Encryption ]

[ Counter mode Decryption ]
Counter방식은 블록 암호를 스트림 암호로 바꾸는 구조를 지닌다고 한다. 어려운 내용은 아니고 ECB에서 각 블록의 번호를 가지고 암호화를 한다. 단, 매번 암호화 할 때 마다 위 그림처럼 Nonce가 달라진다는 것이 문제이다. Nonce는 비표 라고도 하며 Nonce + Counter를 가지고 사용이 된다.

서버에 전송한 예시를 보면 매번 값이 달라지는 것을 알 수 있다.

************ MI6 Secure Encryption Service ************

...

Enter your command:
encrypt
Message: hi
0efd85b4b61ddf819b2fce13719f8f3183f48e6070c99f39461f840b65e7933a96f33f6fe9bdac800dd0ff74f1823aec7bde02331495fe808726729cfcf89493a6b4692ad216c5b006eee57d468ded0504f338113ce0207ffb0e6b25f710620cf1f0
root@nopdata:/home/iy/ctf/encrypt2# nc enigma2017.hackcenter.com 32762

************ MI6 Secure Encryption Service ************

...

Enter your command:
encrypt
Message: hi
25a9b3fd25903bbc16efb9ab9558a4c8279bf0b8375d19cdb5fecd10d468ce086f1f250d2e291bad675a3897dcc2e6d8a9b1d9fc0b0849d54feb0e6d5db424abd16163fabf062f2885285f10136c67b41378f443b6beea3a874196ad4bf89f3ad755
root@nopdata:/home/iy/ctf/encrypt2#

동일한 hi라는 메시지를 암호화 시켰음에도 비표값(Nonce)로 인해 결과 값이 달라지는 것을 알 수 있다.
하지만 암호화에 있어서 바뀌는 것은 비표값 뿐이기 때문에 암호화 되는 다른 순서는 동일하다고 볼 수 있다.
또 암호화와 복호화의 루틴이 동일하기 때문에 이를 역으로 이용하면 답을 얻어낼 수 있다.
여기서 더 진행을 하기 위해서는 이전 문제 ECB에서 key를 얻어와야 하는데...

다시 시작!

먼저 소스코드를 다시 보면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/python2 -u
from Crypto.Cipher import AES
from Crypto.Util import Counter
import os
import zlib
 
flag = open("flag""r").read() # You do not have access to this file.
key = open('enc_key''r').read().strip().decode('hex')
 
welcome = """
************ MI6 Secure Encryption Service ************
       ________   ________    _________  ____________;_
      - ______ \ - ______ \ / _____   //.  .  ._______/ 
     / /     / // /     / //_/     / // ___   /
    / /     / // /     / /       .-'//_/|_/,-'
   / /     / // /     / /     .-'.-'
  / /     / // /     / /     / /
 / /     / // /     / /     / /
/ /_____/ // /_____/ /     / /
\________- \________-     /_/
Enter your command:
"""
 
def encrypt():
  iv = os.urandom(16)
  ctr = Counter.new(128, initial_value=long(iv.encode('hex'), 16))
  cipher = AES.new(key, AES.MODE_CTR, counter = ctr)
 
  m = raw_input("Message: ")
  m = "signature=" + flag + m
  m = zlib.compress(m, 9)
 
  encrypted = cipher.encrypt(m)
 
  return iv.encode('hex'+ encrypted.encode('hex')
 
def decrypt():
  m = raw_input("Encrypted Message: ")
 
  iv = m[:32].decode('hex')
  ctr = Counter.new(128, initial_value=long(iv.encode('hex'), 16))
  cipher = AES.new(key, AES.MODE_CTR, counter = ctr)
  compressed = cipher.decrypt(m[32:].decode('hex'))
  m = zlib.decompress(compressed)
 
  if m[10:10 + len(flag)] != flag:
    return "Invalid signature!"
 
  return m[10 + len(flag):]
 
def process(cmd):
  if cmd == "help":
    return "Commands:\n\thelp - this\n\tencrypt - encrypt a message\n\tdecrypt - decrypt a message\n"
  if cmd == "encrypt":
   return encrypt() 
  if cmd == "decrypt":
    return decrypt()
  return "Invalid command. See help for a list of commands\n"
 
= raw_input(welcome)
response = process(m.strip())
print(response)
 
cs

간단하다. 먼저 encrypt, decrypt 명령을 받고 해당 기능을 수행한다. 문제는 이번에는 입력하는 문자가 제일 뒤에 붙어 암호화 되는 메시지는
"signature= 플래그 메시지"가 된다. 따라서 flag가 aaa이고 입력한 값이 bbb면 "signature=aaabbb"가 암호화 되는 것이다.
역시 문제를 푸려면 먼저 flag의 길이값을 알아내야 한다. 이는 메시지를 encrypt해서 돌아오는 return value의 길이를 통해 알 수 있다.

먼저 로컬에서 소스코드를 변경하여 암호화 한 경우는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@nopdata:/home/iy/hackcenter/output# python cr.py
Message: 1
message :  signature=aaabbb1 17
iv length : 32, encrypted length : 50 
893212bdd28499500ff464bfc2cbfbad195a9db35b3d3e9374906903d06d9519ccab25da67b1bcefba
root@nopdata:/home/iy/hackcenter/output# python cr.py
Message: 11
message :  signature=aaabbb11 18
iv length : 32, encrypted length : 52 
09f267469948f5405fad59fdf4d61e01df12a53d16f6b15b1a7a7fa989dae6d7326989a3c2848e493b5f
root@nopdata:/home/iy/hackcenter/output# python cr.py
Message: 111
message :  signature=aaabbb111 19
iv length : 32, encrypted length : 54 
630193089da6f31086d8b376a1fd6b5870656106df0ab256244d25b57720af037de60d09422e8db9dd7a74
root@nopdata:/home/iy/hackcenter/output
 
cs

랜덤하게 생기는 초기화벡터 (Nonce)값의 길이는 32이며 메시지 암호화되는 메시지 길이를 확인해야 하는데.... 위에서 signature까지 붙여서 총 18자리의 메시지를 암호화시키면 초기화 벡터 값의 길이까지 84자리가 결과로 나온다.

하지만 서버에 2자리만 넣어서 보내면 결과로 총 196자리가 돌아온다. 초기화 벡터 길이를 제외하면 164의 길이가 암호화된 값으로 나오게 되는 것이다.
로컬에서 테스트로 메시지의 길이를 50000으로 해서 넣으면 그 결과로 144의 암호화 데이터를 받을 수 있는데 그렇다면 flag의 길이가 50000자리 이상이라는건가...?

Commit a crime to steal the flag.
The length of the ciphertext is leaking information
이게 힌트인데.... 암호문의 길이가 단서라......

알아보니 crime attack이라는 공격 방법을 사용해야 한다. 때문에 파일 이름도 관련이 없는 crime.py로 지은 듯 하다.

crime attack은 tls/ssl에 적용되는 공격 기법으로 전송되는 암호문의 길이를 기반 공격을 하는 방식이다. 위에서 18자리를 넣어도 54밖에 되지 않는 암호문의 길이를 받은 이유는 중복된는 문자열이 많기 때문이다. 이는 zlib의 압축의 영향이다. 전송되는 메시지에 특정 문자열이 중복된다면, 압축 알고리즘에 의해 결과 길이도 크게 나오지 않는다.
그 결과를 보면 다음과 같다.



Enter your command:
encrypt
Message: signature
62b4de4c717210981228361b148af3b0a7c25cd3fbe29f29b123529ece2b5f7390286b6342157df43696a7a8c83b5660f03e6d12d6cd39f7fd08e3fcb6b84b826203df2fbda968f6c0c047f7e2a0bba419047c59f563a64f0f09562ffd56189fb5377b
-> 결과 암호문 길이 : 198

Enter your command:
encrypt
Message: zxcvbnmlk
fee4ffad02cb6adc583e3f51620b6f7561ae2d5ee69a7822046eeae4146cf95a1435a5fd6934226021f87f0926e28a77d66edc2c3a0206f5534b573029cffa8184377f899a94f4cd5aeb2dde98e1dab6e01992e9370c1958875fb9e760bb2dd8b7c92c8cb20d599c
-> 결과 암호문 길이 : 208

signature와 zxcvbnmlk의 메시지 길이는 똑같다. 하지만 돌아온 결과 암호문의 길이는 큰 차이를 보인다. 이는 암호화된 평문에서 'signature' 문자열이 중복 되었기 때문에 zlib에서 압축 결과를 줄인 것이다. 이 단서를 기반으로 결과로 오는 메시지 길이값을 알아내면 된다. flag의 길이의 경우를 아직 모르는데 일단 최대치는 알 수 있다.

서버에 메시지를 아무것도 보내지 않을 경우 서버는 "signature=(flag)"를 암호화 할 것이다. 이 결과로 돌아오는 암호문의 길이는 총 192. 여기서 iv길이를 빼주면 160이 된다.
로컬에서 평문에 중복 하나 없이 메시지를 넣었을 때 72자리의 평문을 넣었을 때 암호문의 길이가 160이 된다. 물론 중복이 하나도 없다는 가정.
근데 소스를 짜고 보네 에러 처리를 해서 그냥 flag길이 제한은 생각하지 않기로 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from pwn import log
import socket
import string
 
 
table = string.printable
 
message = "signature="
= log.progress("message ")
while True:
        sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        sock.connect(("enigma2017.hackcenter.com",32762))
        tmp = sock.recv(1024)
        tmp = sock.send("encrypt\n")
        tmp = sock.recv(1024)
        tmp = sock.send(message+"\xff\n")
        tmp = sock.recv(1024)
        limit_len = (len(tmp)/2)*2
        chk = True
        for ch in table:
                p.status("%s %s"%(message, ch))
                sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
                sock.connect(("enigma2017.hackcenter.com",32762))
                tmp = sock.recv(1024)
                tmp = sock.send("encrypt\n")
                tmp = sock.recv(1024)
                tmp = sock.send(message+ch+"\n")
                tmp = sock.recv(1024)
                if len(tmp) < limit_len:
                        message += ch
                        chk = False
                        break
        if chk:
                print "error"
                break
 
p.success(message)
 
cs


flag : 9a36f0315e042cbba4739860410e015e_compress_then_encrypt_right?!

ref

유사문제
plaidctf 2013 compression