[CryptoHack] Bean Counter
Background
파이크립토의 카운터 모드가 제가 원하는 대로 작동하는 데 어려움을 겪었기 때문에 ECB 모드를 CTR로 직접 전환했습니다. 제 카운터는 위아래로 움직일 수 있어서 암호 분석가들을 따돌릴 수 있습니다! 그들이 제 사진을 읽을 수 있는 가능성은 전혀 없습니다.
분석
카운터 모드? CTR? 이게 무슨 말일까
from Crypto.Cipher import AES
KEY = ?
class StepUpCounter(object):
def __init__(self, value=os.urandom(16), step_up=False):
self.value = value.hex()
self.step = 1
self.stup = step_up
def increment(self):
if self.stup:
self.newIV = hex(int(self.value, 16) + self.step)
else:
self.newIV = hex(int(self.value, 16) - self.stup)
self.value = self.newIV[2:len(self.newIV)]
return bytes.fromhex(self.value.zfill(32))
def __repr__(self):
self.increment()
return self.value
@chal.route('/bean_counter/encrypt/')
def encrypt():
cipher = AES.new(KEY, AES.MODE_ECB)
ctr = StepUpCounter()
out = []
with open("challenge_files/bean_flag.png", 'rb') as f:
block = f.read(16)
while block:
keystream = cipher.encrypt(ctr.increment())
xored = [a^b for a, b in zip(block, keystream)]
out.append(bytes(xored).hex())
block = f.read(16)
return {"encrypted": ''.join(out)}
문제 코드는 StepUpCounter라는 클래스와 encrypt 함수를 제공한다.
encrypt에서 ECB모드를 사용하고 ctr이라는 StepUpCounter객체를 생성한다.
StepUpCounter은 value로 random의 hex값을 가지고 step을 1로 가지고, stepup은 False를 가진다.
bean_flag.png파일을 이진 파일로 열어서 block 변수에 16byte씩 read한다. ctr.increment()를 인자로 하여 ECB로 암호화하고,
keystream과 block끼리 xor을 수행한다.
결과로 출력되는 값은 블록마다 xor을 수행하고 나온 결과이다.
StepUpCounter class의 incremet함수는 stup이 False이면 랜덤인 value값에서 빼서 IV를 생성하고.. 아니 근데 왜 boolean인 stup을 빼는거지.. 오타인가..
그냥 CTR 모드를 찾아보는게 나을 거 같다.
이건 CTR 모드에 대한 이미지이다.
CTR 모드는 CounTeR 모드이다. 1씩 증가해가는 카운터를 암호화해서 키 스트림을 만들어낸다.
흠.. 저번에 했던 OFB와 비슷하지만, 다른 점이라면 OFB는 iv를 암호화하고 계속 그 iv로 부터 나온 암호화 값을 또 iv로 사용했다면,
CTR모드는 기본 iv로부터 1씩 더해간 값으로 암호화를 하고 xor을 수행하는 것 같다.
OFB처럼 스트림을 사용하기 때문에, 복호화에서도 동일하게 iv를 알고 있으면 1씩 더하고 암호화하여 xor하면 된다. 암, 복호화가 동일하게 수행되고 오류 전파가 일어나지 않는다.
xor 시에 블록 길이가 16byte보다 작으면 어떻게 될까?
OFB나 CTR은 패딩이 필요 없다는 말이 있길래..
풀이
encrypted에는 뭐가 들어있는지 생략했다. 사이트에서 submit을 클릭하면 나오는 그 긴 16진수 맞다.
바이트 안 맞으면 어쩌지 하면서 살펴본 것
여기서 길이를 재보면 뭔가 안 맞는다.
바이트 단위로 //2 하고 %16 해보면 11이 나온다. 마지막 블록이 11byte라는 뜻이다. 5byte가 없다.
16바이트씩 잘라서 출력해도 뒤 5byte가 보이지 않는다. 이미 있는 곳 까지만 xor 하나보다.
xor을 뚫기 위해서 이미 알고 있는 값이 어떻게 암호화되었는지를 알아야 한다.
마침 png파일이 주어졌는데 png 파일의 헤더를 알고 있으므로 복호화 가능하지 않을까
png 시그니처가 8byte이다. 우리는 16byte가 필요하다.
png파일의 구조를 자세히 보니, IHDR라는 청크가 PNG 앞에 위치한다고 한다. 처음에는 IHDR length = 00 00 00 0D 가 들어있고, IHDR = 49 48 44 52가 들어있다고 한다. 이렇게 되면 16byte를 구할 수 있다. width 정보나 height 정보는 때에 따라 바뀌겠지만 앞 0x10은 변하지 않을 것이다.
생각해보니 xor하는 값을 하나 알아낸다고 해서 모든 블록을 복호화할 수 있는게 아니다. 각자 iv에서 +1된 값을 가지고 AES를 거쳤기 때문이다.
여기서는 오타? 코드 상의 오류가 있었다. 오타인줄 알았는데 이게 의도된거였다니. 너무 뻔하게 보이는데, CTR을 증가시키는 부분이 작동하지 않는다. 처음에 False로 주었기 때문이다. 이렇게 되면 모든 코드가 초기 iv를 AES 한 동일한 값으로 xor된다.
encheader = bytes.fromhex(encrypted[:32])
pngheader = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52'
key = xor(encheader, pngheader)
encrypted = bytes.fromhex(encrypted)
decrypted = b''
for i in range(0, len(encrypted), 16):
decrypted += xor(encrypted[i : i+16], key)
with open('decrypted.png', 'wb') as f:
f.write(decrypted)
그럼 그냥 xor암호 아닌가.