Cypher: 고급 퍼즐을 풀기 위한 Python 코드(3-4, 4-3, 5-*, 6-5)

Python을 사용하여 고급 퍼즐(3-4, 4-3, 5-1, 5-2, 5-3, 6-5)을 해결한 방법

 

개요

이 가이드는 Python의 도움으로 고급 퍼즐을 어떻게 풀었는지 보여줍니다. 원래는 연필로 퍼즐을 풀고 스크린샷을 인쇄했습니다(아래 3-3 이미지와 같습니다. 스포일러가 되지 않으려면 확대/축소하지 마세요). 그러나 나중에 문제를 위해 시행착오에 지쳤습니다. 그래서 파이썬을 사용하게 되었습니다.

참고: 내 코드를 Steam 커뮤니티에 게시하려면 목록 인덱스의 변수 이름을 i에서 x로 바꿔야 마크업 태그로 구문 분석되지 않습니다(제 생각에 마크업은 코드 요소 내에서 무시되어야 함).

3-4: 주파수 분석

아래 코드는 각 문자의 빈도를 계산합니다. 완료되면 출력 및 ETAOIN SHRDLU에 따라 각 문자를 할당하면 THE를 식별할 수 있습니다. 그런 다음 문제는 연필과 종이로 쉽게 해결됩니다.
from collections import Counter

text = """
AJLPNYRJZJFLZYASGSKQGSMME
JKEJPFSVLPJLKKEJELNNSPZKY
NNYGNSGASGYZJGAKEYZYGASVW
NJKSWSKECNLUJZKEJZEYCYZWS
VOEKLGAHYKKJAZEJNYJZLKLGU
ESPPJLAFHSPZJLFSVGJRJPYTL
OYGJALZMJJKJPZUESSGJPLUEY
NATYOEKZLYNEJPKMSEVGAPJAK
SGZGLTJEYZCLGYSNL
""".replace("\n", "")

print(Counter(text))


결과(스포일러를 원하지 않으면 확대/축소하지 마십시오):

4-3: 주파수 분석

다알파벳 치환 암호(Vigenère cipher)를 풀기 위해 가장 먼저 해야 할 일은 키의 길이를 파악하는 것입니다. 게임 내 힌트에 따르면 DUF와 LUE라는 두 개의 반복되는 3글자 시퀀스가 ​​있습니다. 각각의 출현 범위를 세고 최대 공약수를 취하면 가장 가능성이 높은 키 길이: 3을 얻습니다.

키의 길이가 식별되면 단일 알파벳 대체 암호에서와 같이 빈도 분석 기술을 사용할 수 있습니다. 아래 코드는 적용되는 키의 문자에 따라 암호문을 "단계"로 나누고 단계 내 문자의 빈도를 계산합니다.

from collections import Counter

text = """\
LAFLUIWOYWPADUFHSNBVSWVNDZQDUF
RBPLUYQPLWLPHZRLUEDUBSYMIPRDIJ
HTYQUCUZYLKFRSKHZBUHULUEKPQFOY
LYSSAMWOCWHZOLGDTDDPPOFDDTGOPY
UDGWOYOSDRYKVVDVLAULRZYGWPLJZY
QKYPTWVLJIAFHHSWOMUVDDAPLMJLUE
PVLRNPDWFXWMQAFHZSEQCFAGQDFLJF
LHLDSWCLMQLFXUBULBDUBVPVWFQHWY
UHRHJGSOCUZZXAGFVLILQVAFDARKPQ
LZCQAGULJBUCZAMPL\
""".replace("\n", "")

phase1 = "".join(text[x] for x in range(len(text)) if x % 3 == 0)
phase2 = "".join(text[x] for x in range(len(text)) if x % 3 == 1)
phase3 = "".join(text[x] for x in range(len(text)) if x % 3 == 2)

triplets = (text[x - 2] + text[x - 1] + text[x] for x in range(2, len(text), 3))

print(Counter(phase1))
print(Counter(phase2))
print(Counter(phase3))
print(Counter(triplets))


코드는 또한 DUF 및 LUE와 같이 반복되는 3글자 시퀀스를 찾으려고 시도합니다. 1-2-3 이외의 단계에서 시퀀스를 찾을 필요가 없다고 생각했습니다. 불행히도 평문에서 "THE"가 무엇인지 찾기에는 충분하지 않았습니다.

4-3: 무차별 대입 공격

주파수 분석에 따라 약간의 교대를 시도한 후, 나는 내 컴퓨터를 시험해보기로 결정했습니다.

코드는 솔루션에 대한 여러 후보를 출력하므로 올바른 후보를 수동으로 선택해야 합니다.

from string import ascii_uppercase as alphabets
from itertools import product

text = """\
LAFLUIWOYWPADUFHSNBVSWVNDZQDUF
RBPLUYQPLWLPHZRLUEDUBSYMIPRDIJ
HTYQUCUZYLKFRSKHZBUHULUEKPQFOY
LYSSAMWOCWHZOLGDTDDPPOFDDTGOPY
UDGWOYOSDRYKVVDVLAULRZYGWPLJZY
QKYPTWVLJIAFHHSWOMUVDDAPLMJLUE
PVLRNPDWFXWMQAFHZSEQCFAGQDFLJF
LHLDSWCLMQLFXUBULBDUBVPVWFQHWY
UHRHJGSOCUZZXAGFVLILQVAFDARKPQ
LZCQAGULJBUCZAMPL\
""".replace("\n", "")

def char_to_num(char):
    return alphabets.index(char) + 1

def vigenere_sub(char1, char2):
    return alphabets[(char_to_num(char1) - char_to_num(char2) - 1) % 26]

for key_chars in product(alphabets, repeat=3):
    decryption = ""
    for x in range(len(text)):
        decryption += vigenere_sub(text[x], key_chars[x % 3])
    if "THE" in decryption and "AND" in decryption:
        print(key_chars)
        print(decryption)
        print()


이미지의 오른쪽 상단 부분에서 알 수 있듯이 무차별 대입 공격의 결과 빈도 분석으로 퍼즐을 거의 풀었다는 것을 알지만 진행에 대한 확신이 없어 포기했습니다. (스포를 원하지 않으시면 이미지를 확대/축소하지 마세요.)

5-1, 5-2, 5-3

기계화된 암호화 퍼즐의 경우 Enigma의 기능을 에뮬레이트하는 코드를 작성할 수 있습니다. 다음을 제외하고 동일한 코드를 사용하여 모든 문제를 해결할 수 있습니다.

  • 두 번째 퍼즐의 경우 디스크의 초기 회전을 결정하기 위해 약간의 시행착오 또는 약간의 무차별적인 힘이 필요합니다.
  • 마지막 퍼즐의 경우 몇 쌍의 문자를 교환하는 기능이 필요합니다.
from string import ascii_uppercase as keyboard

scrambler1 = (keyboard, "UWYGADFPVZBECKMTHXSLRINQOJ")
scrambler2 = (keyboard, "AJPCZWRLFBDKOTYUQGENHXMIVS")
scrambler3 = (keyboard, "TAGBPCSDQEUFVNZHYIXJWLRKOM")
reflector = (keyboard, "YRUHQSLDPXNGOKMIEBFZCWVJAT")

def scramble(position, disk, backwards=False):
    row1, row2 = disk[::-1] if backwards else disk
    char = row1[position]
    return row2.index(char)

def rotate(disk, n):
    return (disk[0][n:] + disk[0][:n], disk[1][n:] + disk[1][:n])

def decrypt(char, *disks):
    x = keyboard.index(char)
    for disk in disks:
        x = scramble(x, disk)
    for disk in disks[-2::-1]:
        x = scramble(x, disk, True)
    return keyboard[x]

def decrypt_text(text, *disks):
    decryption = ""
    disk1 = disks[0]
    for char in text:
        disk1 = rotate(disk1, 1)
        decryption += decrypt(char, disk1, *disks[1:])
    return decryption

def swap(text, key):
    result = ""
    for char in text:
        result += key.replace(char, "") if char in key else char
    return result

def swap_keys(text, keys):
    for key in keys:
        text = swap(text, key)
    return text

def puzzle_5_1():
    print(decrypt_text("ZYDNI", scrambler1, reflector))

def puzzle_5_2():
    for i in range(26):
        scrambler = rotate(scrambler1, i)
        text = decrypt_text("QHSGUWIG", scrambler, reflector)
        if text[:2] == "XV":
            print(text)

def puzzle_5_3():
    text = "GYHRVFLRXY"
    keys = ("AB", "SZ", "UY", "GH", "LQ", "EN")
    text = swap_keys(text, keys)
    d1 = rotate(scrambler2, scrambler2[0].index("A"))
    d2 = rotate(scrambler1, scrambler1[0].index("E"))
    d3 = rotate(scrambler3, scrambler3[0].index("B"))
    text = decrypt_text(text, d1, d2, d3, reflector)
    text = swap_keys(text, keys)
    print(text)

puzzle_5_1()
puzzle_5_2()
puzzle_5_3()


6-5

퍼즐은 간단합니다. 알고리즘을 거꾸로 실행하기만 하면 됩니다. 그러나 이것을 손으로 해결하면 하나의 작은 실수로 모든 것이 엉망이 될 수 있습니다. 이것이 내가 이 문제를 해결하기 위해 Python을 사용(또는 사용해야 했습니다)한 이유입니다.

알고리즘을 재해석했음을 주목하십시오. 텍스트와 키 모두에 행 교체를 적용한 다음 XOR 처리하는 대신, 먼저 명령을 XOR 처리한 다음 행을 교체하는 것으로 해석했습니다. 결과는 다음과 같습니다. 똑같다.

def block(bitstr):
    return (bitstr[:4], bitstr[4:8], bitstr[8:12], bitstr[12:])

def swap(rows):
    return tuple(row[1] + row[0] + row[3] + row[2] for row in rows)

def xor(rows, key):
    keybits = "".join(bin(ord(char))[2:].zfill(8) for char in key)
    if len(keybits) != 16: raise AssertionError
    textbits = "".join(rows)
    val = "".join("0" if keybits[x] == textbits[x] else "1" for x in range(16))
    return block(val)

def shift(rows):
    result = []
    for n in range(len(rows)):
        row = rows[n]
        result.append(row[n:] + row[:n])
    return tuple(result)

def decrypt(cipher, key):
    return xor(swap(shift(cipher)), key)

ciphertext = block("1001011110110101")
first = decrypt(ciphertext, "BX")
second = decrypt(first, "YS")
print(chr(int(second[0] + second[1], 2)))
print(chr(int(second[2] + second[3], 2)))

By unvollstaendigkeitssatz

태그

코멘트 남김

ArabicEnglishFrenchGermanItalianJapaneseKoreanPortugueseSpanish