0. 3분 요약
Affine Cipher에서 모든 바이트가 같은 식으로 암호화된다면 key를 전부 브루트포스하지 않아도 됩니다.
c = key1 * p + key2 mod 251평문에 들어간 문자열 일부를 알고 있다면, 서로 다른 두 바이트만으로 key1, key2를 계산할 수 있습니다.
문제 코드에는 다음 힌트가 있었습니다.
assert b'cryptography' in secret따라서 암호문에서 cryptography가 들어갈 수 있는 위치만 훑고, 해당 위치의 앞 두 글자로 key를 계산하면 됩니다.
1. 문제 정보
- 문제: Dreamhack Affe!n
- 분야: crypto
- 난이도: Level 1
- 핵심 키워드: Byte-Affine cipher, Known Plaintext Attack
Dreamhack 설명은 “카이사르의 시대는 갔습니다. 이제는 Byte-Affine cipher의 시대입니다.”라고 되어 있습니다.
이 문제는 byte 단위 Affine Cipher를 사용하고, 평문에 들어 있는 단어 하나를 힌트로 key를 복원하는 실습 문제입니다.
2. 고등학생도 이해할 수 있는 원리
이 암호는 글자 하나하나를 아래 공식으로 바꿉니다.
암호문 = key1 * 평문 + key2 를 251로 나눈 나머지코드로 보면 다음과 같습니다.
c = (key1 * p + key2) % 251여기서 우리가 모르는 값은 key1, key2 두 개입니다.
그런데 문제 코드에는 이런 힌트가 있습니다.
assert b'cryptography' in secret즉 원래 문장 안에 "cryptography"라는 단어가 반드시 들어 있습니다.
예를 들어 암호문 어딘가에 "cryptography"가 암호화된 결과가 있다고 생각해봅시다. 평문 첫 글자 'c', 두 번째 글자 'r'을 숫자로 보면 다음과 같습니다.
'c' = 99
'r' = 114암호문에서 그 위치의 두 바이트가 각각 c0, c1이라고 하면 식이 두 개 생깁니다.
c0 = key1 * 99 + key2 mod 251
c1 = key1 * 114 + key2 mod 251이제 두 식을 빼면 key2가 사라집니다.
c0 - c1 = key1 * (99 - 114) mod 251남은 미지수는 key1 하나뿐입니다. 그래서 key1을 먼저 구하고, 원래 식에 다시 넣어서 key2도 구할 수 있습니다.
key1 = ((c0 - c1) * pow(99 - 114, -1, 251)) % 251
key2 = (c0 - key1 * 99) % 251정리하면 다음 순서입니다.
- 평문 안에
cryptography가 있다는 것을 안다. - 암호문에서
cryptography가 들어갈 수 있는 위치를 하나씩 맞춰본다. - 그 위치의 앞 두 글자로
key1을 계산한다. key1로key2도 계산한다.- 계산한 key로
cryptography전체가 제대로 암호화되는지 확인한다. - 맞으면 그 key가 정답이다.
그래서 전체 key를 전부 때려 맞히는 brute force가 아니라, 알려진 단어 하나를 이용해서 방정식처럼 key를 푼 것입니다.
이런 방식을 Known Plaintext Attack, 즉 “알려진 평문 공격”이라고 부릅니다.
3. 문제 코드 핵심
key1, key2 = random.randint(1, 250), random.randint(1, 250)
def encrypt(self, msg):
msg_enc = b""
for b in msg:
msg_enc = msg_enc + bytes([(self._key1 * b + self._key2) % 251])
return msg_enc각 byte p는 다음과 같이 암호문 byte c로 변환됩니다.
251은 소수이기 때문에, 0이 아닌 값은 모두 역원을 가집니다.
4. 브루트포스가 필요 없는 이유
평문과 암호문의 한 쌍을 알고 있으면 식은 다음처럼 됩니다.
하지만 unknown이 key1, key2 두 개라서 한 쌍만으로는 부족합니다.
서로 다른 평문 byte 두 개를 알면 됩니다.
두 식을 빼면 key2가 사라집니다.
따라서 key1은 다음처럼 구할 수 있습니다.
그리고 key2는 다시 원래 식에 대입하면 됩니다.
5. 풀이 코드
from pathlib import Path
ct = bytes.fromhex(Path("output.txt").read_text().split("> ")[1].strip())
known = b"cryptography"
for off in range(len(ct) - len(known) + 1):
p0, p1 = known[0], known[1]
c0, c1 = ct[off], ct[off + 1]
key1 = ((c0 - c1) * pow(p0 - p1, -1, 251)) % 251
key2 = (c0 - key1 * p0) % 251
if all((key1 * p + key2) % 251 == c for p, c in zip(known, ct[off:off + len(known)])):
print("offset:", off)
print("key1:", key1)
print("key2:", key2)
pt = bytes(pow(key1, -1, 251) * (c - key2) % 251 for c in ct)
print(pt)
break6. 실행 결과
해당 문제의 output.txt 기준 결과는 다음과 같습니다.
offset: 74
key1: 79
key2: 87즉 전체 key 공간을 도는 대신, cryptography가 시작될 수 있는 위치만 확인하면 됩니다.
7. 실습: Dreamhack Affe!n 풀이
실습 목표
Dreamhack Affe!n 문제에서 제공된 prob.py와 output.txt만 가지고 random으로 생성된 key1, key2를 구하고, 암호문을 복호화합니다.
이 문제에서 중요한 파일은 두 개입니다.
prob.py: 암호화 로직이 들어 있는 문제 코드output.txt: 암호화된 문장이 들어 있는 출력 파일
1단계: 암호화 식 확인
prob.py의 핵심은 이 부분입니다.
bytes([(self._key1 * b + self._key2) % 251])즉 byte 하나가 다음 식으로 암호화됩니다.
c = key1 * p + key2 mod 251여기서 p는 평문 byte, c는 암호문 byte입니다.
2단계: 알려진 평문 찾기
문제 코드에는 다음 줄이 있습니다.
assert b'cryptography' in secret따라서 원래 문장 안에는 cryptography라는 단어가 반드시 들어 있습니다.
이제 암호문 안에서 cryptography가 들어갈 수 있는 위치를 하나씩 가정하면 됩니다.
3단계: key 계산하기
cryptography의 첫 두 글자는 c, r입니다.
'c' = 99
'r' = 114어떤 위치에서 암호문 byte 두 개가 c0, c1이라면 다음 식이 생깁니다.
c0 = key1 * 99 + key2 mod 251
c1 = key1 * 114 + key2 mod 251두 식을 빼면 key2가 사라집니다.
c0 - c1 = key1 * (99 - 114) mod 251그래서 key1을 먼저 구할 수 있습니다.
key1 = ((c0 - c1) * pow(99 - 114, -1, 251)) % 251그 다음 key2는 원래 식에 대입합니다.
key2 = (c0 - key1 * 99) % 2514단계: exploit.py 작성
from pathlib import Path
MOD = 251
KNOWN = b"cryptography"
def main():
ct = bytes.fromhex(Path("output.txt").read_text().split("> ", 1)[1].strip())
for off in range(len(ct) - len(KNOWN) + 1):
p0, p1 = KNOWN[0], KNOWN[1]
c0, c1 = ct[off], ct[off + 1]
key1 = ((c0 - c1) * pow(p0 - p1, -1, MOD)) % MOD
key2 = (c0 - key1 * p0) % MOD
chunk = ct[off : off + len(KNOWN)]
if all((key1 * p + key2) % MOD == c for p, c in zip(KNOWN, chunk)):
pt = bytes(pow(key1, -1, MOD) * (c - key2) % MOD for c in ct)
print(f"offset = {off}")
print(f"key1 = {key1}")
print(f"key2 = {key2}")
print(pt.decode())
return
raise SystemExit("known plaintext not found")
if __name__ == "__main__":
main()5단계: 실행 결과 확인
python3 exploit.py결과는 다음과 같습니다.
offset = 74
key1 = 79
key2 = 87복호화된 문장 마지막에는 flag가 나옵니다.
DH{3fcd60d46ff03b6fcd3a6d4c71805b131f8f48af5ffdf4fb4b9ab7cf5a09a4ef}실습에서 배운 점
random으로 key를 만들었다고 해서 항상 안전한 것은 아닙니다.
이 문제는 다음 조건 때문에 key를 쉽게 구할 수 있었습니다.
- 같은 key가 모든 byte에 반복 사용된다.
- 암호화 식이 선형이다.
- 평문 일부인
cryptography를 알고 있다.
즉 암호에서 randomness보다 중요한 것은 구조입니다. 구조가 단순하고 힌트가 있으면, key가 random이어도 수식으로 복원될 수 있습니다.
8. 핵심 정리
Affine Cipher는 선형식입니다.
그래서 평문과 암호문 일부가 대응되는 순간 key가 수식으로 복원됩니다.
이 공격은 Known Plaintext Attack입니다.
- 평문 일부를 알고 있다.
- 같은 key가 모든 위치에 반복 사용된다.
- 암호화 식이 선형이다.
이 조건이 만족되면 random key라도 안전하지 않습니다.