어떤 수업을 찾고 있나요?

예: PowerShell, 네트워크, XSS, Step 138

전체 커리큘럼 둘러보기 →

개발 도구 · 프로젝트

Step 90. 프로젝트 — XOR 파일 암호화/복호화 도구

Step 90예상 실습 시간 · 3시간

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간

전제: Step 41~46의 파이썬 기초(리스트, 파일 입출력), Step 54의 비트 연산, Step 64의 명령행 인자를 안다.

  • 준비물: 파이썬, 텍스트 에디터, 아무 파일이나 하나(암호화 실험용 — 중요한 파일은 절대 아닌 것으로).
  • 주의: 오늘 만드는 암호는 교육용입니다. 실전에서 쓸 수 없다는 사실까지 스스로 증명하는 것이 오늘의 핵심 목표입니다. 오늘의 “깨기” 실험은 전부 여러분이 직접 암호화한 파일에만 적용합니다.

암호라고 하면 첩보 영화의 검은 상자를 떠올리지만, 그 심장에는 놀랄 만큼 단순한 연산이 있습니다. 오늘의 주인공 XOR(배타적 논리합, 기호 ⊕) — 규칙은 단 한 줄, “같으면 0, 다르면 1.” 이 연산에는 마법 같은 성질이 있습니다: 어떤 값 A를 키 K로 XOR한 뒤 다시 K로 XOR하면 원래의 A로 돌아옵니다. 이 성질 하나로 “키를 아는 사람만 되돌릴 수 있는 변환”이 만들어집니다. 오늘의 목표는 두 가지입니다 — XOR로 파일을 암호화하고 복호화하는 도구를 직접 만들고, 이 암호가 왜 약한지 스스로 깨부수는 것. 만들고, 부수고, 이유를 설명하는 세 박자가 보안 공부의 가장 전형적인 리듬입니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • XOR의 복원 성질(A ⊕ K ⊕ K = A)을 파이썬 ^ 연산으로 보여 준다
  • "rb"/"wb" 바이너리 모드로 파일을 읽고 쓴다
  • 명령행 인자를 받는 enc/dec CLI 도구를 완성한다
  • 256번 전수 시도(브루트포스)로 한 바이트 키 암호를 깬다
  • 키 재사용이 왜 위험한지 실험으로 증명하고, 이 암호의 한계를 설명한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3 (스크립트 + 한 줄 실행)
오늘의 도구 ^(XOR 연산자), bytes(), open(..., "rb"/"wb"), sys.argv, range(256)
필요한 개념 XOR 진리표, 대칭키, 바이너리 파일, 키 공간, 알려진 평문 공격
오늘의 산출물 xor_tool.py(암복호화 도구), crack.py(전수 시도 크래커)

2-1. XOR의 마법 성질 — 스위치를 두 번 누르기

XOR의 진리표는 이렇습니다.

A B A ⊕ B
0 0 0
0 1 1
1 0 1
1 1 0

여기서 성질을 하나만 외우면 됩니다. A ⊕ K ⊕ K = A. 전등 스위치를 같은 방식으로 두 번 누르면 원상태인 것과 같습니다. 파이썬에서 XOR은 ^ 기호입니다.

2-2. 대칭키 — 같은 열쇠로 잠그고 여는 암호

오늘의 방식처럼 암호화할 때와 복호화할 때 같은 키를 쓰는 암호를 대칭키 암호라고 부릅니다. 현관 열쇠와 같습니다 — 잠글 때 쓴 열쇠로 열 때도 엽니다. 반대 개념은 공개키 암호(잠그는 열쇠와 여는 열쇠가 다름)로, 나중 단계에서 만납니다.

2-3. 바이너리 파일 — 글자가 아니라 바이트

지금까지 open("파일")로 읽은 것은 텍스트였습니다. 하지만 사진, 실행 파일, 압축 파일에는 “글자로 해석할 수 없는 바이트”가 있습니다. 이런 파일을 다룰 때는 모드에 b를 붙입니다 — "rb"(바이너리 읽기), "wb"(바이너리 쓰기).

바이너리로 읽으면 파이썬은 파일을 바이트의 나열(bytes 객체)로 줍니다. 각 바이트는 0~255의 숫자입니다. 그리고 XOR은 숫자끼리 하는 연산이죠 — 모든 파일은 숫자 나열이고, 숫자는 XOR할 수 있다. 이것이 “어떤 파일이든 암호화할 수 있다”는 말의 실체입니다.

2-4. 스트림 암호의 뿌리와 키 공간

단일 바이트를 반복하는 XOR는 가역 변환 실습이며 안전한 암호화가 아닙니다. 현대 스트림 암호는 비밀 키와 방식에 맞는 nonce·카운터로 키스트림을 만듭니다. nonce 처리와 인증도 중요하므로 실제 데이터에는 검토된 인증 암호화 라이브러리를 사용하세요.

그리고 오늘의 또 다른 주인공이 키 공간(가능한 키의 개수)입니다. 한 바이트 키의 키 공간은 256개. 실전 암호 AES-128은 2의 128제곱 개입니다. 이 차이가 3-7에서 “1초 만에 깨지는가, 우주의 나이보다 오래 걸리는가”를 가릅니다.


3. 따라 하기

3-1. XOR 성질 확인 — 마법의 첫 목격

입력:

python -c "print(65 ^ 42); print((65 ^ 42) ^ 42)"

출력 (2026-09-09 실측):

107
65

읽는 법: 65(문자 ‘A’)를 42로 뒤집으면 107(‘k’)이 되고, 같은 42로 다시 뒤집으니 65로 돌아왔습니다. 107이라는 중간값만으로는 키 42 없이 원래가 뭔지 알 수 없습니다.

왜 하는가: 오늘 만드는 모든 것이 이 두 줄 위에 서 있습니다. 직접 눈으로 확인한 성질만이 믿을 수 있는 도구가 됩니다.

3-2. 바이너리로 파일 읽기

입력: 실험용 파일을 만들고 읽어 봅니다.

echo "hello secret" > test.txt
python -c "data = open('test.txt','rb').read(); print(data); print(list(data)[:5])"

출력 (2026-09-09 실측):

b'hello secret\n'
[104, 101, 108, 108, 111]

읽는 법: b'...'는 바이트 나열이라는 표시입니다. 뒤의 리스트가 각 바이트의 숫자값입니다 — h는 104, e는 101. 파일이 숫자들이라는 말이 증명되었습니다. 암호화는 “이 숫자들을 전부 XOR로 뒤집는 것”이 될 것입니다.

3-3. 암호화 함수 — 한 줄의 핵심

입력: xor_tool.py를 만들기 시작합니다.

def xor_bytes(data, key):
    return bytes([b ^ key for b in data])

읽는 법: 리스트의 각 바이트 b를 key와 XOR해 새 바이트 나열을 만듭니다. bytes([...])는 숫자 리스트를 바이트로 포장합니다. 이 한 줄이 암호화이자 복호화입니다 — XOR은 대칭이라 함수가 하나면 됩니다. 복잡해 보이는 “암호화 프로그램”의 심장이 사실 이 한 줄이고, 나머지 코드는 전부 파일 읽기·쓰기와 인자 처리라는 포장지입니다.

3-4. CLI로 완성하고 왕복하기

입력: xor_tool.py 전체를 완성합니다.

"""Reversible single-byte XOR toy. Never use for confidential data."""
import argparse
from pathlib import Path


def xor_bytes(data, key):
    if not 0 <= key <= 255:
        raise ValueError("key must be 0..255")
    return bytes(b ^ key for b in data)


def byte_key(value):
    key = int(value)
    if not 0 <= key <= 255:
        raise argparse.ArgumentTypeError("key must be 0..255")
    return key


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("mode", choices=("enc", "dec"))
    parser.add_argument("input", type=Path)
    parser.add_argument("key", type=byte_key)
    args = parser.parse_args(argv)
    target = Path(str(args.input) + "." + args.mode)
    try:
        # Exclusive creation refuses to overwrite an existing output.
        with args.input.open("rb") as source, target.open("xb") as out:
            while chunk := source.read(65536):
                out.write(xor_bytes(chunk, args.key))
    except OSError as exc:
        parser.exit(1, f"File operation failed: {exc}\n")
    print(f"{args.mode} complete: {target}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

입력 (실행):

python xor_tool.py enc test.txt 42
python xor_tool.py dec test.txt.enc 42
cat test.txt.enc.dec
cmp test.txt test.txt.enc.dec

출력 (2026-09-09 실측):

enc 완료: test.txt.enc
dec 완료: test.txt.enc.dec
hello secret

cmp는 아무 출력 없이 끝났습니다 — 두 파일이 바이트 단위로 완전히 같다는 뜻입니다 (Windows에는 fc /b가 있습니다).

argparse는 파일 처리 전에 모드·경로·바이트 키를 검증합니다. XOR은 양방향에 같은 연산을 사용합니다. 바이트 단위 왕복 일치는 해당 입력의 가역성을 검증할 뿐 기밀성이나 변조 탐지를 증명하지 않습니다.

흥미로운 발견 (2026-09-09 실측): 암호문 test.txt.enc를 od -c로 들여다보면 B O F F E ...처럼 우연히 읽히는 글자들이 나옵니다. ‘h'(104)와 키 42의 XOR이 ‘B’이기 때문입니다. 암호문이 “이상한 기호”일 필요는 없다는 것 — 암호문은 그저 “키 없이는 의미를 알 수 없는 바이트”입니다.

3-5. 예측해 보기 — 키가 틀리면 어떻게 될까

암호화할 때 키 42를 쓰고, 복호화할 때 키 43을 쓰면 결과는?

  • (a) 대충 비슷하게 복원된다
  • (b) 완전히 엉뚱한 바이트가 나온다
  • (c) 오류가 발생한다

직접 확인:

python -c "data=open('test.txt.enc','rb').read(); print(bytes([b ^ 43 for b in data]))"

출력 (2026-09-09 실측):

b'idmmn!rdbsdu\x0b'

틀린 XOR 키는 별도 인증 없이 다른 바이트를 만듭니다. 42 XOR 43은 1이므로 각 바이트의 최하위 비트가 뒤집힙니다. 키 숫자의 차이가 작다는 사실로 텍스트 유사도를 예측할 수 없습니다. 뺄셈이 아니라 XOR 비트 마스크로 설명하세요.

3-6. 깨부수기 — 256번의 전수 시도

내 도구를 내가 공격해 봅니다. 키를 모른다고 가정합시다.

입력: crack.py:

data = open('test.txt.enc', 'rb').read()
for key in range(256):
    plain = bytes([b ^ key for b in data])
    if b'hello' in plain:
        print(f"키 후보: {key} → {plain[:20]}")

출력 (2026-09-09 실측):

키 후보: 42 → b'hello secret\n'

읽는 법: 키 후보 256개를 전부 돌려 “읽히는 결과”를 찾았습니다. 1초도 안 걸립니다. 이것이 오늘 도구가 “교육용”인 이유의 증명입니다 — 키 공간이 256개인 암호는 컴퓨터 앞에서 무방비입니다.

왜 하는가: “키가 길어야 한다”는 교과서 문장의 이유를 방금 손으로 확인했습니다. AES-128의 키 공간(2의 128제곱)이 왜 필요한지 더 설명이 필요 없을 것입니다.

3-7. 키 재사용의 위험 — 같은 키로 두 번 암호화하면

XOR 암호에는 전수 시도 말고도 더 교묘한 약점이 있습니다. 같은 키로 두 메시지를 암호화하면, 두 암호문을 XOR하는 순간 키가 상쇄됩니다: C1 ⊕ C2 = (M1 ⊕ K) ⊕ (M2 ⊕ K) = M1 ⊕ M2.

입력:

key = 77
m1 = b'attack at dawn'
m2 = b'retreat at noon'
c1 = bytes([b ^ key for b in m1])
c2 = bytes([b ^ key for b in m2])
print('c1 XOR c2 == m1 XOR m2:', bytes(a ^ b for a, b in zip(c1, c2)) == bytes(a ^ b for a, b in zip(m1, m2)))
print('복구된 키:', c1[0] ^ ord('a'))   # 첫 글자가 'a'임을 안다면

출력 (2026-09-09 실측):

c1 XOR c2 == m1 XOR m2: True
복구된 키: 77

읽는 법: 키를 전혀 모르는데도 두 암호문의 XOR이 두 평문의 XOR과 정확히 일치했습니다 — 키가 지워진 것입니다. 게다가 평문의 한 글자라도 알면(예: 메시지가 “attack”으로 시작한다는 추정) 키가 그대로 복구됩니다. 이것을 알려진 평문 공격(known-plaintext attack)이라고 합니다.

서로 다른 메시지에 같은 키스트림을 재사용하지 마세요. 현대 스트림 암호는 요구되는 nonce·카운터 규칙을 따르면 한 키로 여러 메시지를 암호화할 수 있습니다. 일회용 패드의 비밀 난수 패드는 다르며 절대로 재사용하면 안 됩니다.

3-8. 눈으로 보는 암호화 — 바이너리 파일 왕복

텍스트 말고 이진 파일로 실험하면 범용성이 눈에 보입니다. 실험용 이미지(PPM 형식)를 파이썬으로 직접 만들어 봅시다.

입력:

python -c "
w,h=64,64
with open('photo.ppm','wb') as f:
    f.write(b'P6\n64 64\n255\n')
    for y in range(h):
        for x in range(w):
            f.write(bytes([x*4 % 256, y*4 % 256, 128]))
print('photo.ppm 생성')"
python xor_tool.py enc photo.ppm 200
python xor_tool.py dec photo.ppm.enc 200
cmp photo.ppm photo.ppm.enc.dec

출력 (2026-09-09 실측):

photo.ppm 생성
enc 완료: photo.ppm.enc
dec 완료: photo.ppm.enc.dec

cmp가 다시 침묵했습니다 — 이미지 파일도 바이트 단위로 완벽히 왕복했습니다. 암호화된 photo.ppm.enc는 헤더(P6)까지 뒤집혔기 때문에(실측: od -c로 보면 앞 바이트들이 8진수 230 376 302 ...로 변함) 어떤 이미지 뷰어도 열지 못합니다.

읽는 법: 텍스트든 그림이든 파일은 바이트 나열이고, 바이트는 XOR됩니다. “암호화하면 프로그램도 못 알아본다”는 것이 어떤 상태인지 체감하는 실험입니다.


4. 미션과 연습문제

미션 — 완성형 도구와 자기 분석

  1. xor_tool.py가 enc/dec 모두 동작하고, 왕복 결과가 원문과 일치하는지 cmp(또는 fc /b)로 증명합니다
  2. 텍스트가 아닌 파일(이미지, 압축 파일 등)에도 왕복 성공시킵니다
  3. crack.py로 자기 암호문을 깨서 키를 찾아냅니다
  4. Step 89의 위키에 XOR암호.md를 씁니다 — 한 줄 요약 / 사용 명령 / 막혔던 점 / “왜 약한가” 3줄 설명
  5. 보강 도전(선택): 한 바이트 대신 여러 글자짜리 키를 받아 바이트마다 돌아가며 XOR하게 고치고, 그래도 여전히 약한 이유를 정리합니다

연습문제

문제 1. A ⊕ K ⊕ K = A가 성립하는 이유를 진리표의 어느 줄에서 읽을 수 있는지 설명해 보세요.

문제 2. 암호화·복호화에 같은 함수 하나면 충분한 이유를 “대칭키”라는 단어와 함께 설명해 보세요.

문제 3. 한 바이트 키 XOR 암호가 1초 만에 깨지는 이유와, AES-128이 전수 시도로 깨지지 않는 이유를 “키 공간”으로 비교해 보세요.

문제 4. 같은 키로 암호화한 두 암호문을 XOR하면 무슨 일이 일어나며, 이것이 왜 위험한지 설명해 보세요.


해설과 완료 기준 · 펼치기/접기

5. 모범 답안과 완료 기준

미션 모범 답안

도구는 3-4의 코드가 완성형입니다. 미션의 검증 명령만 모으면:

python xor_tool.py enc test.txt 42
python xor_tool.py dec test.txt.enc 42
cmp test.txt test.txt.enc.dec
# crack.py reads test.txt.enc and searches for the known dummy word hello.
python crack.py
# Use a fresh folder or new output names when repeating; existing outputs are preserved.

선택 과제(여러 글자 키)의 뼈대:

def xor_bytes(data, key):
    return bytes([b ^ key[i % len(key)] for i, b in enumerate(data)])

i % len(key)로 키의 바이트를 돌아가며 씁니다. 그래도 약한 이유: 키 공간이 커지기는 하지만, 키가 반복되는 한 같은 키 바이트가 주기적으로 다시 쓰이고(3-7의 키 재사용 문제가 주기마다 반복), 키 길이를 추정해 나누어 전수 시도하는 공격이 가능합니다.

검증하는 법: ① cmp가 침묵하는가. ② crack.py가 올바른 키를 찾는가. ③ 위키 문서에 “왜 약한가”가 적혔는가. 전부 ‘예’이면 완성입니다.

연습문제 해답

문제 1 해답. 진리표에서 K=0이면 A ⊕ 0 = A(1·3행: 그대로), K=1이면 A ⊕ 1 = A의 반전(2·4행)입니다. 즉 XOR은 “K=1인 자리만 뒤집는” 연산이라, 같은 K로 두 번 뒤집으면 뒤집힌 자리가 다시 뒤집혀 원래대로 돌아옵니다.

문제 2 해답. XOR은 같은 키로 두 번 적용하면 원래대로 돌아오는 대칭 연산이므로, 암호화와 복호화가 같은 계산입니다. 잠글 때와 열 때 같은 열쇠를 쓰는 대칭키 암호이기 때문에 함수 하나로 양방향을 커버합니다.

답 3: 장난감 예제의 바이트 키는 256가지뿐이어서 전수 탐색이 쉽습니다. AES-128의 키 공간은 훨씬 크지만 크기만으로 보안이 성립하지는 않습니다. 구성·구현·키 관리·인증도 중요합니다.

답 4: 같은 키스트림을 재사용하면 평문끼리의 XOR이 노출됩니다. 이 단일 바이트 장난감에서는 알려진 평문 한 바이트로 반복 키를 찾습니다. 현대 스트림 암호는 고유 nonce·counter 규칙에 따라 비밀키를 사용할 수 있지만 키스트림은 재사용하면 안 됩니다.

완료 기준 체크리스트

  • [ ] ^ 연산으로 XOR의 복원 성질을 보여 줄 수 있다
  • [ ] 바이너리 모드("rb"/"wb")로 파일을 읽고 쓸 수 있다
  • [ ] enc/dec이 동작하는 CLI 도구를 완성했다
  • [ ] 왕복 결과가 원문과 같음을 cmp로 증명했다
  • [ ] 256번 전수 시도로 자기 암호문을 깼다
  • [ ] 키 재사용 공격(M1 ⊕ M2 노출)을 설명할 수 있다
  • [ ] 이 암호가 약한 이유(키 공간, 키 반복)를 말할 수 있다

6. 흔한 실수와 해결

벽 1. 복원 파일이 깨진다 / UnicodeDecodeError가 난다

증상 (2026-09-09 실측, 암호문을 텍스트 모드로 읽은 경우):

UnicodeDecodeError: 'cp949' codec can't decode byte 0xfe in position 6: illegal multibyte sequence

원인: open에 b를 빠뜨려 텍스트 모드로 읽은 것입니다. 텍스트 모드는 운영체제가 인코딩(한국어 Windows는 cp949)을 해석하다가 바이너리 바이트에서 죽거나 바꿔 버립니다.
해결: 읽기·쓰기 모두 "rb"/"wb"인지 확인하세요. 이 b 하나가 이 챕터 최다 사고입니다.

벽 2. “ValueError: invalid literal for int()”가 난다

증상 (2026-09-09 실측, 키 자리에 key라는 글자를 넣은 경우, 경로는 가공):

  File "xor_tool.py", line 10, in main
    mode, path, key = sys.argv[1], sys.argv[2], int(sys.argv[3])
ValueError: invalid literal for int() with base 10: 'key'

예전 traceback은 직접 int 변환할 때의 예시입니다. 교정한 완성 CLI는 argparse로 정수가 아니거나 0–255 밖인 값을 출력 파일 생성 전에 거부합니다. 사용법을 확인하고 유효한 바이트 키로 다시 실행하세요.

벽 3. crack.py가 아무 후보도 못 찾는다

증상: 256번 돌렸는데 조용합니다.
원인: 암호문에 찾으려는 단어(hello)가 없거나, 그 단어가 원문에 없었습니다.
해결: 원문에 실제로 있는 단어로 찾으세요. 실전에서는 “영어 문장처럼 보이는가”에 점수를 매기는 방식을 씁니다 — 짧은 단어 하나가 우연히 맞는 오탐도 있으니 출력된 후보를 눈으로 확인하는 것까지가 크래킹입니다.

벽 4. 틀린 키인데 오류가 안 나와서 모른 채 넘어간다

이 장난감은 인증이 없어 틀린 키로도 바이트가 나옵니다. 42와 43의 XOR 차이는 1이라 각 바이트의 최하위 비트를 뒤집지만 숫자상 가까움이 일반적인 문자 유사도 척도는 아닙니다. 실습에서는 원본과 비교하고 읽힌다는 이유로 진위를 믿지 마세요.

벽 5. 파일 이름이 눈덩이처럼 붙는다 (.enc.enc.dec…)

증상: 반복 실행할수록 확장자가 쌓입니다.
원인: 출력 이름을 단순히 이어 붙이는 방식의 한계입니다.
해결: dec 모드에서는 끝의 .enc를 떼어내는 식으로 이름 규칙을 다듬어 보세요. 이 마무리까지가 “도구의 완성도”입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
XOR (⊕) 같으면 0, 다르면 1. A ⊕ K ⊕ K = A — 이 성질이 암호를 만든다
대칭키 암호 잠글 때와 열 때 같은 열쇠를 쓰는 암호
바이너리 모드 "rb"/"wb" — 모든 파일은 0~255 숫자의 나열
키 공간 가능한 키의 개수. 256개면 전수 시도 1초, 2^128이면 불가능
전수 시도(브루트포스) 키 후보를 전부 돌려 읽히는 것을 고르는 공격
알려진 평문 공격 평문 일부를 알면 키가 복구되는 공격 — 키 재사용 시 치명적
OTP 키가 데이터만큼 길고 무작위이며 한 번만 쓰이는, 수학적으로 깰 수 없는 유일한 암호

오늘의 코드

코드 하는 일
a ^ b 두 숫자의 XOR
bytes([b ^ key for b in data]) 바이트 나열 전체를 키로 뒤집기 (암호화=복호화)
open(p, "rb").read() 파일을 바이트 나열로 읽기
open(p, "wb").write(b) 바이트 나열을 파일로 쓰기
sys.argv[1:] 명령행 인자 꺼내기
for key in range(256) 한 바이트 키 전수 시도
cmp 원본 복원본 왕복 검증 (무출력=일치, Windows는 fc /b)

명령어보다 중요한 감각

오늘 여러분은 만들고, 되돌리고, 스스로 깼습니다. XOR 한 줄이 암호가 되고, 키 공간 256이 그 암호의 무덤이 되며, 키 재사용이 암호문 두 개를 맞물려 평문을 새어 나가게 한다는 것까지 전부 손으로 확인했습니다. “약한 암호를 약한 줄 알고 쓰는 것”과 “모르고 쓰는 것”의 차이가 전문가와 아마추어의 차이입니다.

일회용 패드는 메시지 이상 길이의 비밀 균등 난수 패드를 한 번만 쓴다는 엄격한 조건에서 완전 비밀성을 제공합니다. 메시지 인증은 제공하지 않습니다. 단일 바이트 실습은 긴 난수 패드 조건을 충족하지 않습니다. 복호 실습은 제공한 가짜 데이터나 허용된 CTF 자료에만 적용하세요.


전부 체크되면 Step 90 완료입니다.

한 단계 더 가까이

수업을 마쳤나요?

본문의 완료 기준을 확인한 뒤 직접 표시하세요.

화면이나 링크에 문제가 있나요? 알려주세요.
다음 수업