标签 Crypto 下的文章

RSA 加密体制破译

来源:“RSA 大礼包”——首届(2016)全国高校密码数学挑战赛 赛题三

截获数据处理

首先按照赛题描述的帧结构提取所有的截获数据:

1024bit 模数 N | 1024bit 加密指数 e | 1024bit 密文 m^e mod N
Frame0:
N = 90058705186558569935261948496132914380077312570281980020033760044382510933070450931241348678652103772768114420567119848142360867111065753301402088676701668212035175754850951897103338079978959810673297215370534716084813732883918187890434411552463739669878295417744080700424913250020348487161014643951785502867
e = 46786465362686334917265996843779843233606992585424976481745055335468678697948774988450305612127967926533923268260412557000125153569622340353246096040604284883505587337829322949633637609180797447754513992039018904786537115087888005528547900640339270052628915440787357271345416818313808448127098885767015748889
c = 48641173720475702278690317652676924796340996697567087705119344461991930773386153198223372579328462635803653561516674380209276666328375805315553713680858906705068657158073776194628700821011001144559278784795978097710145192236347629751116534400207288736776545247409895672030976932673010818369814246455196991083
--------
Frame1:
N = 92921790800705826977497755832938592891062287903332844896046168726101016067456726822505517352409138948392871113192427210529297191908638888388136391240683157994654207338463678065440899870434887094216772312358731142317774259942199808535233769089985063860828267808621928898445383706310204223006136919334252875849
e = 65537
c = 1626661141529320283833484152716550848856697186049377493478368799832043379420727509223318694347625977694500761460048670101820769656612419734057871562023463159698522348510157125720014700549254630959391701883372400982386084212421115166791728704867253734354874934210987301137512341070190760227227749365878233484
--------
Frame2:
N = 90252653600964453524559669296618135272911289775949194922543520872164147768650421038176330053599968601135821750672685664360786595430028684419411893316074286312793730822963564220564616708573764764386830123818197183233443472506106828919670406785228124876225200632055727680225997407097843708009916059133498338129
e = 65537
c = 39632263504870478574861695051251850807454294787974709214866410237055871793939895562441267574482198916367858789237648434983815369123479208726344716594227785308836601932181727610859898951190206345056426253251079929822424252271957269630987623886812686545521745791771387808772030614435314730783528512800343192265
--------
Frame3:
N = 92270627783020341903769877272635163757611737252302329401876135487358785338853904185572496782685853218459404423868889360808646192858060332110830962463986164014331540336037718684606223893506327126112739408023014900003600028654929488487584130630596342720833061628867179840913592694993869009133576053124769728363
e = 5
c = 83421434286602546493364204139182949897795123160498680670964040331447569764445309937195566103281638928183742488663157138572020817924561990979723444797045375101801354862472761507421896454904818874439231990567738173059815647539737800523632262742398190575822391771655932895657208471832891505814792263361394479317
--------
Frame4:
N = 90058705186558569935261948496132914380077312570281980020033760044382510933070450931241348678652103772768114420567119848142360867111065753301402088676701668212035175754850951897103338079978959810673297215370534716084813732883918187890434411552463739669878295417744080700424913250020348487161014643951785502867
e = 152206992575706893484835984472544529509325440944131662631741403414037956695665533186650071476146389737020554215956181827422540843366433981607643940546405002217220286072880967331118344806315756304650248634546597784597963886656422706197757265316981889118026978865295597135470735576032282694348773714479076093197
c = 19560634556305755550927540610989537766715902244072312818350844104485773927537226443429404190213856361759564153804627450805880512600339869169513348929194643809859468549718922965997647689203029517135396008631050292544022651948009392475583045438153697076529266662217519588521116539517972522591294232192817502376
--------
Frame5:
N = 99193711547257063160816850544214924340574358752670644615293764532335872088470223740970673347993652626497557387222167784182876395436088845281840169701654629849214222297784511349059698963212947299142320497759258889425182705042123217476724761095690092179821753840224757786599021225709340258545979566824267620959
e = 65537
c = 26054677793581772924866273737009673285775062802734786532404396138990264566536537921648515854012553861999940229349708989519156563830916553754762208466745321226835312974971739761769324569315525872096987367001543758380071859429619580182411498650200401467760546057912183435480924905200466941116258838789328064564
--------
Frame6:
N = 146839643970016464813197409569004275595828791825722617066607993001682901023784267554815946189374651530288894322286859792246413142980277245909181062525398546369553995023529451396820549308690493928593324007689135648753323161394735120908960458860801743476353228970081369439513197105039143930008573928693059198131
e = 65537
c = 47190775807472506173587993082023759909601357229808667044044468676457696140445235738005020994278091230440755033222450219378047807646817722376918364211727971804312327204294555178996480944188624972632371674822397258127227029990196956900925820980263353418653201918881814896866168764140848945600419602253279143149
--------
Frame7:
N = 155266493936043103849855199987896813716831986416707080645036022909153373110367007140301635144950634879983289720164117794783088845393686109145443728632527874768524615377182297125716276153800765906014206797548230661764274997562670900115383324605843933035314110752560290540848152237316752573471110899212429555149
e = 3
c = 124929943232081828105808318993257526364596580021564021377503915670544445679836588765369503919311404328043203272693851622132258819278328852726005776082575583793735570095307898828254568015886630010269615546857335790791577865565021730890364239443651479580968112031521485174068731577348690810906553798608040451024
--------
Frame8:
N = 102900163930497791064402577447949741195464555746599233552338455905339363524435647082637326033518083289523250670463907211548409422234391456982344516192210687545692054217151133151915216123275005464229534891629568864361154658107093228352829098251468904800809585061088484485542019575848774643260318502441084765867
e = 5
c = 25585088950095290712328215701309273521406235982885781641137768116285362079062975527364909549362511146004390419156826709543326814581466248280564194951706937822088902607754944405407698735824315900942915322054614437632116732271787823814807624841386886185122143173564380877370643120953803688563589496390425159539
--------
Frame9:
N = 97767951046154372321400443371234495476461828137251939025051233003462769415459435471728054384852461870179980010660162922547425212869925648424741526671585598167502856111641944825179295197098826911226483155821197251989297102189187139234080795582529077092266799813985026581245196104843272305656744384140745492897
e = 65537
c = 14375950543873882011796759348848479283522955796749853113492047625299699702886303193822347995567175524401038661237990847185236138967814088030767785916645492142741397786256445305366822277551514353423864240674522264407918605662008550545442780563568811883349771003546081844527788515420708612431091464410712019656
--------
Frame10:
N = 93836514358344173762895084384953633159699750987954044414830106276642828025218933012478990865656107605541657809389659063108620208004740646099662700112782252200834393363574089818787717951026690934986964275526538236750596344542450864284576226592039259070002692883820960186403938410354082341916474419847211138467
e = 65537
c = 78852785408127338210375705302361611580033298047358566712385067002412358292419274287993295604646693755514055710305938805847184012173449160624823261013152092151242661538772012880714981492275731658527465442787266554947828301571586721387286510359738598116104180351027973922256460236377354127082438812404967605644
--------
Frame11:
N = 112306066601652819062206435724795595603085908011001671184332227488970057128128821831260649058569739569103298091727188365019228385820143813415009397359257831092635374404034997011441653286642458431865026213129412677064308342580757248577955071384972714557250468686599901682728173096745710849318629959223270431039
e = 3
c = 108387832390337770947361518376552702503741092284778824448943971792044922720461955035726863109418657218498659460663504872870862538725835055240750735576735249122665348803252691221869146679004017916359067454693701495389784159620341860394035373599823801288442604273046729873467936004227013186659110262247417571857
--------
Frame12:
N = 90267480939368160749458049207367083180407266027531212674879245323647502822038591438536367206422215464489854541063867946215243190345476874546091188408120551902573113507876754578290674792643018845798263156849027209440979746485414654160320058352559498237296080490768064578067282805498131582552189186085941328701
e = 5
c = 44374979291120575503988741531987454898919254880086464254904878064332010355432423956182135846738023874326776872139229379943321321362822522900479438294291206287205647145759972233097276253408812699557305314344220807356024994977399840843758750494467535572805794732065369887057841293267499209427585419962565568495
--------
Frame13:
N = 94390533992358895550704225180484604016029781604622607833044135524814562613596803297695605669157378162035217814540004231075201420796787547733762265959320018107419058832819010681344133011777479722382525797938558181629835768471461434560813554411133962651212455645589624432040989600687436833459731886703583047283
e = 65537
c = 41663689952657185984513733558388033289292857758748468070934326941659317694408873831451567385012905508903797893149006067280788298408959017459890579859784072677410890657854942639040056924596925599973762214900728648657052474974405878868755028761443878403349272421153452240103741921751653022646614028009138548572
--------
Frame14:
N = 120008876536855131221255979370745233738591934188224528487535120483456214085493237482915446419599357910343450285858995374277365393767669569942204888383426461862651659865189178784473131914234181752055950431093341514138390898892413182538823693941124637301582389014479754627419560568004831093116617428970538503551
e = 65537
c = 35133765260146855599194761500993159592311136378033858818728078464540389548474611501950689942825550399101504201020687961256642455745888410410524955937773951578993882275525944145131794970001708655718844507774877602125183877782393564092461821246419013099835940432551540513624090850765797735157630551978900512155
--------
Frame15:
N = 147733349387696521015664992396355145811249793103958464053225389476050097503928022819269482555955365534137156079172704297584033078453033637103720972881068435459202133846880715879894340131656691631756162323422868846616160423755883726450486845175227682329583615739797782025647376042249605775433971714513081755709
e = 3
c = 52253817590056116368273294519761274350847193477090280916373828903718796358618956145225746496960677477661151583828604021049936963779103440560630451125137344639503705880024677345063113240530798352727432768980751992926293801206779839157443722614687126711272753610923903360818026083573711899014859313677159790039
--------
Frame16:
N = 90673177193017332602781813187879442725562909473411994052511479411887936365983777106776080722300002656952655125041151156684340743907349108729774157616323863062525593382279143395837261053976652138764279456528493914961780300269591722101449703932139132398288208673556967030162666354552157189525415838326249712949
e = 5
c = 24086371701602948122317790211004032014326487279907486724991846810668564197542368948703436295770758262739732290677177527297040556666434577730354732397784651220918412407485171180732327730242552955646750279842251200227937257322414662213662054605527282812231172173474061845763736546747711105935349033514358348526
--------
Frame17:
N = 111178307033150739104608647474199786251516913698936331430121060587893564405482896814045419370401816305592149685291034839621072343496556225594365571727260237484885924615887468053644519779081871778996851601207571981072261232384577126377714005550318990486619636734701266032569413421915520143377137845245405768733
e = 65537
c = 1395222187334055833498435136007269572138525113145744882969531037442244086277594803865217301719947066153176244638660864035949705664670633245110847416168796640199238733478540080417312141011028469385167826450855601412915611725028631975605932279023918771764204031806414734015476034106891049334159757621016327648
--------
Frame18:
N = 93394639108667212482180458616036741615058981058942739509025631675767304945732437421192075466824789572910657586684470553691049259504106442090140927782673066834126848556317079995332229262871079799089771973100731889841015960713908117908583988637159206246729697336281050046919985463146705713899703248595045701819
e = 65537
c = 49047978458885807127192385282227726754593888749388775377492411121925201201621099927332087316607446894372751446254341808051569111053293066232980434901592875347200122022210780536817524813076908750647137301610117592355818408280291766068780616226847056325075159440352473034526412778650516438709293396458312728764
--------
Frame19:
N = 94154993593274109828418786834159728190797445711539243887409583756844882924221269576486611543668906670821879426307992404721925623741478677756083992902711765865503466687919799394258306574702184666207180530598057989884729154273423032471322027993848437082723045300784582836897839491321003685598931080456249945287
e = 65537
c = 52958695992371180409414011678115981405835026800648278393085136639708219930134280877954018305615378579281651249142230848262822421713895227069561145945972448893229231020632492517869034217943260664130647322694583182800800838539691542175229797652856708373533581250607375664993806654537737027000328299623032632769
--------
Frame20:
N = 90916739755838083837461026375700330885001446224187511395518230504776419813625940046511904838818660297497622072999229706061698225191645268591198600955240116302461331913178712722096591257619538927050886521512453691902946234986556913039431677697816965623861908091178749411071673467596883926097177996147858865293
e = 5
c = 23204039098754030513954332212496652705175644349879686639449689791620605370809827884267260830136516742466455588549253481016504796674014871020503543639681251834114159250986728840380777774144853925216884802529230212783759821262799845229436535491711201551797166082529740271577684082458494926929260818927584104158

破解过程

  1. 从参数选择已经可以看到一些问题:

    • Frame0 和 Frame4 模数相同,考虑共模攻击(成功);

      ​ Frames[0, 4]: Seq 0, Text My secre

    • Frame3、8、12、16、20 指数相同,考虑广播攻击(成功);

      ​ Frames[3, 8, 12, 16, 20]: Seq 1, Text t is a f

    • Frame7、11、15 指数为 3,考虑小指数攻击爆破 k(失败);
    • Frame4 指数非常大,则 d 较小,考虑维纳攻击(已破解,无意义)。
  2. 另外我们还可以尝试分解模数质因子,采用 GCD、费马小定理、Pollard(生日悖论)算法:

    • Frame1 和 18 成功被 GCD 分解,模数共享一个素因子;

      ​ Frame 1: Seq 11, Text . Imagin

      ​ Frame 18: Seq 10, Text m A to B

    • Frame10 成功被费马小定理分解,模数素因子 p、q 相近;

      ​ Frame 10: Seq 8, Text will get

    • Frame2、6、19 成功被 Pollard p-1 算法分解。

      ​ Frame 2: Seq 6, Text That is

      ​ Frame 6: Seq 7, Text "Logic

      ​ Frame 19: Seq 5, Text instein.

  3. 根据赛题描述的明文填充方式和已破解的明文,我们知道明文高位有固定的标志位 0x9876543210abcdef 和范围极小的顺序索引(0~20)以及 0 填充,且 Frame7、11、15 指数为 3,考虑 Coppersmith 攻击(成功)。

    分片明文填充为 512 比特消息后再进行加密,填充规则为高位添加 64 比特标志位,随后加上 32 比特通信序号,再添加若干个 0,最后 64 比特为明文分片字符对应的 ASCII 码

    ​ Frame 7: Seq 2, Text amous sa

    ​ Frame 11: Seq 3, Text ying of

    ​ Frame 15: Seq 4, Text Albert E

  4. 最终 Frame5、9、13、14、17 无法在合理时间内破解。但可以根据顺序索引和已破解的明文上下文猜测其明文:

    ​ Frame5: Seq 12, Text ation wi

    ​ Frame9: Seq 13, Text ll take

    ​ Frame13: Seq 14, Text you ever

    ​ Frame14: Seq 9, Text you fro

    ​ Frame17: Seq 15, Text ywhere."

完整明文

My secret is a famous saying of Albert Einstein. That is "Logic will get you from A to B. Imagination will take you everywhere."

源代码

from sys import set_int_max_str_digits
from os import path
from dataclasses import dataclass
from collections import defaultdict
from concurrent.futures import ProcessPoolExecutor
from itertools import combinations
from gmpy2 import mpz, iroot, gcd, invert, powmod, is_prime
from sage.all import PolynomialRing, Zmod

DATA_DIR = '附件3-2(发布截获数据)'
FLAG_HEX = "9876543210abcdef"
FLAG_VAL = int(FLAG_HEX, 16)


@dataclass
class Frame:
    idx: int
    n: mpz
    e: mpz
    c: mpz


def load_frames() -> list[Frame]:
    frames = []
    for i in range(21):
        with open(path.join(DATA_DIR, f'Frame{i}'), 'r') as file:
            content = file.read().strip()
            payload = bytes.fromhex(content)
            n = mpz(int.from_bytes(payload[0:128], 'big'))
            e = mpz(int.from_bytes(payload[128:256], 'big'))
            c = mpz(int.from_bytes(payload[256:], 'big'))
            frames.append(Frame(i, n, e, c))
    return frames


def parse_plaintext(m: mpz) -> tuple[int, int, str]:
    text_val = int(m & ((1 << 64) - 1))
    text_bytes = text_val.to_bytes(8, 'big').replace(b'\x00', b'')
    text = text_bytes.decode('ascii')
    seq_val = int((m >> 416) & ((1 << 32) - 1))
    flag_val = int((m >> 448) & ((1 << 64) - 1))
    return flag_val, seq_val, text


def solve_coppersmith(frame: Frame, known_high_bits_val: int, unknown_bits: int = 64) -> int:
    n, e, c, k = int(frame.n), int(frame.e), int(
        frame.c), int(known_high_bits_val)
    P = PolynomialRing(Zmod(n), names='x')
    x = P.gen()
    f = (k + x)**e - c
    f = f.monic()
    roots = f.small_roots(X=2**unknown_bits, beta=1.0)
    if roots:
        return int(roots[0])
    return None


def solve_hastad(frames: list[Frame]) -> mpz:
    e = frames[0].e
    n_list = [f.n for f in frames]
    c_list = [f.c for f in frames]
    M = mpz(1)
    for n in n_list:
        M *= n
    result = mpz(0)
    for i in range(len(frames)):
        Mi = M // n_list[i]
        yi = invert(Mi, n_list[i])
        result = (result + c_list[i] * Mi * yi) % M
    m, exact = iroot(result, e)
    return m if exact else None


def pollard_pm1(n: mpz, B: int = 200000) -> mpz:
    a = mpz(2)
    for i in range(2, B):
        a = powmod(a, i, n)
        d = gcd(a - 1, n)
        if 1 < d < n:
            return d
    return None


def pollard_rho(n: mpz, c_start: int = 1) -> mpz:
    if is_prime(n):
        return None
    x = mpz(2)
    y = x
    c = mpz(c_start)
    g = mpz(1)
    count = 0
    while g == 1:
        x = (x * x + c) % n
        y = (y * y + c) % n
        y = (y * y + c) % n
        g = gcd(abs(x - y), n)
        count += 1
        if count > 2000000:
            break
        if g == n:
            if c_start > 10:
                return None
            return pollard_rho(n, c_start + 1)
    if g == n or g == 1:
        return None
    return g


def fermat_factor(n: mpz, limit: int = 100000) -> tuple[mpz, mpz]:
    res = iroot(n, 2)
    a = res[0] + (1 if res[0]*res[0] < n else 0)
    for _ in range(limit):
        val = a * a - n
        if val >= 0:
            res2 = iroot(val, 2)
            if res2[1]:
                return a - res2[0], a + res2[0]
        a += 1
    return None, None


def wiener_attack(frame: Frame) -> tuple[int, str, mpz, mpz]:
    set_int_max_str_digits(100000)
    n, e = frame.n, frame.e
    cf_expansion = []
    n_val, d_val = e, n
    while d_val != 0:
        q = n_val // d_val
        cf_expansion.append(q)
        n_val, d_val = d_val, n_val % d_val

    p0, p1 = mpz(0), mpz(1)
    q0, q1 = mpz(1), mpz(0)

    for q_coeff in cf_expansion:
        pn = q_coeff * p1 + p0
        qn = q_coeff * q1 + q0
        p0, p1 = p1, pn
        q0, q1 = q1, qn

        k, d = pn, qn
        if k == 0:
            continue
        if (e * d - 1) % k != 0:
            continue
        phi = (e * d - 1) // k

        b = n - phi + 1
        delta = b*b - 4*n
        if delta >= 0:
            res = iroot(delta, 2)
            if res[1]:
                x1 = (b - res[0]) // 2
                x2 = (b + res[0]) // 2
                if x1 * x2 == n:
                    return frame.idx, 'Wiener', x1, x2
    return frame.idx, None, None, None


def attempt_factorization(frame: Frame) -> tuple[int, str, mpz, mpz]:
    idx, method, p, q = wiener_attack(frame)
    if method:
        print(f"    Frame {frame.idx} N factored by Wiener")
        return idx, method, p, q

    p, q = fermat_factor(frame.n)
    if p:
        print(f"    Frame {frame.idx} N factored by Fermat")
        return frame.idx, 'Fermat', p, q

    p = pollard_pm1(frame.n)
    if p:
        print(f"    Frame {frame.idx} N factored by Pollard p-1")
        return frame.idx, 'Pollard p-1', p, frame.n // p

    p = pollard_rho(frame.n, c_start=1)
    if p:
        print(f"    Frame {frame.idx} N factored by Pollard Rho")
        return frame.idx, 'Pollard Rho', p, frame.n // p
    return frame.idx, None, None, None


frames = load_frames()
solved = {}

print("\n[1] Common Modulus Attack...")
n_map = defaultdict(list)
for f in frames:
    n_map[f.n].append(f)
for n, group in n_map.items():
    if len(group) > 1:
        def egcd(a, b):
            if a == 0:
                return b, 0, 1
            g, y, x = egcd(b % a, a)
            return g, x - (b // a) * y, y
        g, u, v = egcd(group[0].e, group[1].e)
        if g == 1:
            c1 = invert(group[0].c, group[0].n) if u < 0 else group[0].c
            c2 = invert(group[1].c, group[1].n) if v < 0 else group[1].c
            m = (powmod(c1, abs(u), n) * powmod(c2, abs(v), n)) % n
            flag, seq, text = parse_plaintext(m)
            print(
                f"  Solved Frames {[f.idx for f in group]}: Seq {seq}, Text '{text}'")
            for f in group:
                solved[f.idx] = (seq, text)

print("\n[2] Broadcast Attack...")
e_map = defaultdict(list)
for f in frames:
    e_map[f.e].append(f)
for e, group in e_map.items():
    if e > 20 or len(group) < e:
        continue
    print(f"  Checking e={e} with {len(group)} frames...")
    for subset in combinations(group, e):
        if all(f.idx in solved for f in subset):
            continue
        m = solve_hastad(subset)
        if m:
            flag, seq, text = parse_plaintext(m)
            print(
                f"  Solved Frames {[f.idx for f in subset]}: Seq {seq}, Text '{text}'")
            for f in subset:
                if f.idx not in solved:
                    solved[f.idx] = (seq, text)

print("\n[3] Factorization Attacks...")
for i in range(len(frames)):
    for j in range(i + 1, len(frames)):
        if frames[i].idx in solved and frames[j].idx in solved:
            continue
        g = gcd(frames[i].n, frames[j].n)
        if g > 1 and frames[i].n != frames[j].n:
            p = g
            for f in [frames[i], frames[j]]:
                if f.idx not in solved:
                    q = f.n // p
                    d = invert(f.e, (p-1)*(q-1))
                    m = powmod(f.c, d, f.n)
                    flag, seq, text = parse_plaintext(m)
                    print(
                        f"  Frame {f.idx} Solved (GCD with Frame {frames[j].idx if f == frames[i] else frames[i].idx}): Seq {seq}, Text '{text}'")
                    solved[f.idx] = (seq, text)

print("  Running Fermat & Pollard in parallel...")
tasks = [f for f in frames if f.idx not in solved]
with ProcessPoolExecutor() as executor:
    results = executor.map(attempt_factorization, tasks)
for idx, method, p, q in results:
    if method:
        f = next((fr for fr in frames if fr.idx == idx), None)
        if f:
            d = invert(f.e, (p-1)*(q-1))
            m = powmod(f.c, d, f.n)
            flag, seq, text = parse_plaintext(m)
            print(f"  Frame {idx} Solved ({method}): Seq {seq}, Text '{text}'")
            solved[idx] = (seq, text)

print("\n[4] Coppersmith Attack...")
for f in frames:
    if f.idx in solved or f.e > 3:
        continue
    print(f"  Attempting Frame {f.idx} (e={f.e})...")
    for seq_guess in range(21):
        K = (FLAG_VAL << 448) | (seq_guess << 416)
        root = solve_coppersmith(f, K)
        if root is not None:
            m = K | root
            if powmod(m, f.e, f.n) == f.c:
                flag, seq, text = parse_plaintext(m)
                print(f"    -> MATCH! Seq {seq}, Text '{text}'")
                solved[f.idx] = (seq, text)
                break

print("\n[5] Recover by context...")
context_solutions = {5: (12, "ation wi"), 9: (13, "ll take "), 13: (
    14, "you ever"), 14: (9, " you fro"), 17: (15, 'ywhere."')}
for idx, (seq, text) in context_solutions.items():
    if idx not in solved:
        f = next((fr for fr in frames if fr.idx == idx), None)
        if f:
            text_val = int.from_bytes(text.encode('ascii'), 'big')
            m = (FLAG_VAL << 448) | (seq << 416) | text_val
            if powmod(m, f.e, f.n) == f.c:
                print(
                    f"  Frame {idx} Solved (Context): Seq {seq}, Text '{text}'")
                solved[idx] = (seq, text)

print("\nResult:")
final_seqs = {seq: text for seq, text in solved.values()}
full_text = "".join([final_seqs[s] for s in sorted(final_seqs.keys())])
print(f"Full Message: {full_text}")

题目一

Project Euler RSA Encryption Problem 182

链接:https://projecteuler.net/problem=182

RSA 有可能存在加密但没有加密的情况。

起初遍历所有可能的消息并逐个加密来计算 unconsealed messages count。后来发现有以下公式:

$$ N = (1 + \gcd(e-1, p-1)) \times (1 + \gcd(e-1, q-1)) $$

from gmpy2 import gcd

p = 1009
q = 3643
phi = (p - 1) * (q - 1)

min_unconcealed = p * q
candidates = []

for e in range(3, phi, 2):
    if gcd(e, phi) != 1:
        continue
    count = (1 + gcd(e - 1, p - 1)) * (1 + gcd(e - 1, q - 1))
    if count < min_unconcealed:
        min_unconcealed = count
        candidates = [e]
    elif count == min_unconcealed:
        candidates.append(e)

print(f"min_unconcealed: {min_unconcealed}")
print("e:", end=" ")

for e in candidates[:50]:
    print(f'{e} ', end='')
print()

题目二

Implement RSA

链接:https://www.cryptopals.com/sets/5/challenges/39

只是实现一个简单的 RSA,要求手写 exgcd 和 modinv,那就手写吧。

from gmpy2 import next_prime, powmod
from secrets import randbits
from Crypto.Util.number import bytes_to_long


def gcd(a, b):
    while b != 0:
        a, b = b, a % b
    return a


def ex_gcd(a, b):
    if b == 0:
        return 1, 0
    else:
        x1, y1 = ex_gcd(b, a % b)
        x = y1
        y = x1 - (a // b) * y1
        return x, y


def inverse(a, m):
    x, _ = ex_gcd(a, m)
    return (x % m + m) % m


e = 3
while True:
    p = next_prime(randbits(64))
    q = next_prime(randbits(64))
    n = p * q
    phi = (p - 1) * (q - 1)
    if gcd(e, phi) == 1:
        break

m = bytes_to_long(b'pwnerik.cn')
assert 0 <= m < n
c = powmod(m, e, n)

d = inverse(e, phi)
msg = powmod(c, d, n)
assert m == msg

这个实现有很多问题,例如 e、p、q 太小。

题目一

地址:https://mysterytwister.org/challenges/level-2/aes-key--encoded-in-the-machine-readable-zone-of-a-european-epassport

俄罗斯的电子护照编号编码了一个 AES 密钥,我们需要解码出这个密钥然后解密 AES CBC 模式密文。题目说 IV 初始化为 0,那么我们只需要得到密钥。

目标密文(base64):

9MgYwmuPrjiecPMx61O6zIuy3MtIXQQ0E59T3xB6u0Gyf1gYs2i3K9Jxaa0zj4gTMazJuApwd6+jdyeI5iGHvhQyDHGVlAuYTgJrbFDrfB22Fpil2NfNnWFBTXyf7SDI

题目已经过期了,现在的 ICAO 文件描述的护照编号格式连长度都不一样。这怎么做?只能抄题解了……

题目二

地址:https://cryptopals.com/sets/2

Implement PKCS#7 padding

不需要特别考虑 content 恰好和块等长的情况,相当于再附加一个只有 padding 的块。

def pkcs_7(content: bytes) -> bytes:
    padlen = 16 - (len(content) % 16)
    return content + bytes([padlen] * padlen)


pkcs_7("YELLOW SUBMARINE", 20)

# "YELLOW SUBMARINE\x04\x04\x04\x04"

Implement CBC mode

from base64 import b64decode
from Crypto.Cipher import AES


def aes_ecb_dec(block: bytes, key: bytes) -> bytes:
    assert len(block) % 16 == 0
    cipher = AES.new(key, AES.MODE_ECB)
    return cipher.decrypt(block)


def bytesxor(a: bytes, b: bytes) -> bytes:
    if len(a) > len(b):
        return bytes([x ^ y for (x, y) in zip(a[:len(b)], b)])
    else:
        return bytes([x ^ y for (x, y) in zip(a, b[:len(a)])])


def depkcs_7(content: bytes) -> bytes:
    return content[:len(content) - content[-1]]


def aes_cbc_dec(cipher: bytes, key: bytes, iv: bytes) -> bytes:
    assert len(cipher) % 16 == 0
    result = bytes()
    prev_block = iv
    for i in range(0, len(cipher), 16):
        block = cipher[i:i+16]
        decrypted = aes_ecb_dec(block, key)
        result += bytesxor(decrypted, prev_block)
        prev_block = block
    return depkcs_7(result)


key = b"YELLOW SUBMARINE"
with open('10.txt', 'r') as f:
    encrypted_b64 = f.read()
cipher = b64decode(encrypted_b64)
print(aes_cbc_dec(cipher, key, b'\x00' * 16))

An ECB/CBC detection oracle

通过检查是否有重复块来判断是否是 ECB 模式,用户输入需要有较长的重复 pattern 才能成功。

def aes_ecb_enc(data: bytes, key: bytes) -> bytes:
    assert len(data) % 16 == 0
    cipher = AES.new(key, AES.MODE_ECB)
    return cipher.encrypt(data)


def pkcs_7(content: bytes) -> bytes:
    padlen = 16 - (len(content) % 16)
    return content + bytes([padlen] * padlen)


def aes_cbc_enc(plain: bytes, key: bytes, iv: bytes) -> bytes:
    plain = pkcs_7(plain)
    result = bytes()
    prev_block = iv
    for i in range(0, len(plain), 16):
        block = plain[i: i + 16]
        encrypted = aes_ecb_enc(bytesxor(block, prev_block), key)
        result += encrypted
        prev_block = encrypted
    return result


def randint(a: int, b: int) -> int:
    return randbelow(b - a + 1) + a


def encryption_oracle(plaintext: bytes) -> bytes:
    key = token_bytes(16)
    plaintext = token_bytes(randint(5, 10)) + \
        plaintext + token_bytes(randint(5, 10))

    if randbelow(2) == 0:
        print("Oracle used: ECB")
        ciphertext = aes_ecb_enc(pkcs_7(plaintext), key)
    else:
        print("Oracle used: CBC")
        iv = token_bytes(16)
        ciphertext = aes_cbc_enc(plaintext, key, iv)
    return ciphertext


def detect_mode(ciphertext: bytes) -> str:
    blocks = [ciphertext[i:i+16] for i in range(0, len(ciphertext), 16)]
    if len(set(blocks)) != len(blocks):
        return "ECB"
    return "CBC"


ciphertext = encryption_oracle(input().encode())
mode = detect_mode(ciphertext)
print(f"Detected mode: {mode}")

Byte-at-a-time ECB decryption (Simple)

经典的 ECB Padding Oracle,由于 ECB 对相同 key 相同块的加密无论位置永远相同,我们可以逐字节爆破明文。

由于 key 不变,优化为只初始化 AES cipher 一次。爆破时无需拼接整个 cracked,只需拼接 (padding + cracked)[-15:] 以节省加密时间。

from secrets import token_bytes


unknown = b64decode(
    'Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK')
key = token_bytes(16)
cipher = AES.new(key, AES.MODE_ECB)


def encryption_oracle(plaintext: bytes) -> bytes:
    plaintext = pkcs_7(plaintext + unknown)
    ciphertext = cipher.encrypt(plaintext)
    return ciphertext


# block_size is 16
# Mode is ECB

cracked = b''
for i in range(len(unknown)):
    padding = b'A' * ((16 - 1) - (i % 16))
    encrypted = encryption_oracle(padding)
    start = i // 16 * 16
    target_block = encrypted[start:start + 16]
    for j in range(256):
        if encryption_oracle((padding + cracked)[-15:] + bytes([j]))[:16] == target_block:
            cracked += bytes([j])
            break

print(cracked)
assert cracked == unknown

# b"Rollin' in my 5.0\nWith my rag-top down so my hair can blow\nThe girlies on standby waving just to say hi\nDid you stop? No, I just drove by\n"

ECB cut-and-paste

同上,我们调整用户输入的邮箱地址,构造一个包含 PKCS#7 padded adminuser 这两个块的 k-v 字符串输入。截取加密后 PKCS#7 padded admin 块,替换加密后的最后一个块(PKCS#7 padded user),获得 admin profile 密文。

(如果要验证邮箱地址格式的话这招估计就不管用了。)

def kv_to_dict(input: str) -> dict[str, str]:
    result = dict()
    for entry in input.split('&'):
        k, v = entry.split('=')
        result[k] = v.replace('%26', '&').replace('%3D', '=')
    return result


def dict_to_kv(input: dict[str, str]) -> str:
    entries: list[str] = list()
    for entry in input.items():
        entries.append(
            f'{entry[0]}={entry[1].replace('&', '%26').replace('=', '%3D')}')
    return '&'.join(entries)


current_uid = 0


def profile_for(email: str) -> str:
    global current_uid
    result = dict_to_kv(
        {'email': email, 'uid': str(current_uid), 'role': 'user'})
    current_uid += 1
    return result


print(kv_to_dict('foo=bar&baz=qux&zap=zazzle'))
print(profile_for('foo@bar.com'))

key = token_bytes(16)
cipher = AES.new(key, AES.MODE_ECB)


def send_profile(email: str) -> bytes:
    return cipher.encrypt(pkcs_7(profile_for(email).encode()))


def recv_profile(encrypted: bytes) -> dict[str, str]:
    return kv_to_dict(depkcs_7(cipher.decrypt(encrypted)).decode())


admin_block = pkcs_7(b'admin')
encrypted = send_profile('A' * (16 - len(b'email=')) +
                         admin_block.decode() + 'AAAA')
fake_encrypted = encrypted[:-16] + encrypted[16:32]
print(recv_profile(fake_encrypted))

# {'foo': 'bar', 'baz': 'qux', 'zap': 'zazzle'}
# email=foo@bar.com&uid=0&role=user
# {'email': 'AAAAAAAAAAadmin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0bAAAA', 'uid': '1', 'role': 'admin'}

Byte-at-a-time ECB decryption (Harder)

已知随机添加 5 ~ 10 随机字节。我们可以假设额外的随机 padding 长度就是 10,为了检测这种情况已经发生,我们可以在用户输入的 padding 插入一段 b'B' * block_size,检测只有 B 的加密块是否存在,为了检测这种块我们需要先得到它(利用上文提到的 ECB 缺陷),先输入 b'B' * (block_size * 3) 然后检测重复块,重复的就是只有 B 的块。为了使只有 B 的加密块有可能存在(block_size 字节对齐),我们还需要在“B 块”前添加 b'A' * (block_size - 10)

def hard_encryption_oracle(plaintext: bytes) -> bytes:
    plaintext = pkcs_7(token_bytes(randint(5, 10)) + plaintext + unknown)
    ciphertext = cipher.encrypt(plaintext)
    return ciphertext


def detect_repeat(ecb_enc: bytes) -> int:
    assert len(ecb_enc) >= 32
    for i in range(0, len(ecb_enc) - 16, 16):
        if ecb_enc[i:i+16] == ecb_enc[i+16:i+32]:
            return i


cracked = b''
test = hard_encryption_oracle(b'B' * (16 * 3))
offset = detect_repeat(test)
pure_B_block_enc = test[offset:offset + 16]
for i in range(len(unknown)):
    extra_padding = b'A' * (16 - 10) + b'B' * 16
    padding = b'A' * ((16 - 1) - (i % 16))
    encrypted = bytes()
    while pure_B_block_enc not in encrypted:
        encrypted = hard_encryption_oracle(extra_padding + padding)
    start = i // 16 * 16 + 32
    target_block = encrypted[start:start + 16]
    for j in range(256):
        guess_enc = bytes()
        while pure_B_block_enc not in guess_enc:
            guess_enc = hard_encryption_oracle(
                extra_padding + (padding + cracked)[-15:] + bytes([j]))

        if guess_enc[32:48] == target_block:
            cracked += bytes([j])
            break

print(cracked)
assert cracked == unknown

PKCS#7 padding validation

def depkcs_7(input: bytes) -> bytes:
    padlen = input[-1]
    if 0 < padlen <= 16 or input.endswith(bytes([padlen]) * padlen):
        raise ValueError("Bad padding")
    return input[:-padlen]

CBC bitflipping attacks

You're relying on the fact that in CBC mode, a 1-bit error in a ciphertext block:

  • Completely scrambles the block the error occurs in
  • Produces the identical 1-bit error(/edit) in the next ciphertext block.
KEY = token_bytes(16)
IV = token_bytes(16)


def aes_cbc_enc(plain: bytes, key: bytes, iv: bytes) -> bytes:
    plain = pkcs_7(plain)
    result = bytes()
    prev_block = iv
    for i in range(0, len(plain), 16):
        block = plain[i:i + 16]
        encrypted = aes_ecb_enc(bytesxor(block, prev_block), key)
        result += encrypted
        prev_block = encrypted
    return result


def quote(data: bytes) -> bytes:
    return data.replace(b';', b'%3B').replace(b'=', b'%3D')


def encrypt_userdata(userdata: bytes) -> bytes:
    prefix = b'comment1=cooking%20MCs;userdata='
    suffix = b';comment2=%20like%20a%20pound%20of%20bacon'

    plaintext = prefix + quote(userdata) + suffix
    return aes_cbc_enc(plaintext, KEY, IV)


def check_admin(ciphertext: bytes) -> bool:
    try:
        plaintext = aes_cbc_dec(ciphertext, KEY, IV)
        return b';admin=true;' in plaintext
    except:
        return False


# len(b'comment1=cooking%20MCs;userdata=') -> 32
# len(b';comment2=%20like%20a%20pound%20of%20bacon') -> 42
ciphertext = encrypt_userdata(b'A' * (16 + (48 - 42)))
mask = bytesxor(b'AAAAAA;comme', b';admin=true;')
payload = bytesxor(ciphertext[32:32 + len(mask)], mask)
fake_ciphertext = ciphertext[:32] + payload + ciphertext[32 + len(mask):]
print(check_admin(fake_ciphertext))

# True

题目一

地址:https://www.coursera.org/learn/crypto/assignment-submission/KZ9js/week-1-programming-assignment-optional/attempt

​ 题目使用同一个流密码密钥加密得到 10 个密文,加密方式是逐字节异或,目标是通过这 10 个密文得到密钥以解出第 11 个密文。

​ 已知消息 M、密钥 K,异或加密的过程是

$$ C=M\oplus K $$

得到密文 C。异或是自反的运算,所以解密与加密过程相同

$$ M=C\oplus K $$

​ 对于同一个密钥 K 加密的两个密文 C_1、C_2,如果将两者异或可以得到

$$ B=C_1\oplus C_2=M_1\oplus K\oplus M_2\oplus K=M_1\oplus M_2 $$

字节流B是两消息逐字节异或结果。

​ 我们首先导入这些密文和加密方法

msg_1 = bytes.fromhex('315c4eeaa8b5f8aaf9174145bf43e1784b8fa00dc71d885a804e5ee9fa40b16349c146fb778cdf2d3aff021dfff5b403b510d0d0455468aeb98622b137dae857553ccd8883a7bc37520e06e515d22c954eba5025b8cc57ee59418ce7dc6bc41556bdb36bbca3e8774301fbcaa3b83b220809560987815f65286764703de0f3d524400a19b159610b11ef3e')
msg_2 = bytes.fromhex('234c02ecbbfbafa3ed18510abd11fa724fcda2018a1a8342cf064bbde548b12b07df44ba7191d9606ef4081ffde5ad46a5069d9f7f543bedb9c861bf29c7e205132eda9382b0bc2c5c4b45f919cf3a9f1cb74151f6d551f4480c82b2cb24cc5b028aa76eb7b4ab24171ab3cdadb8356f')
msg_3 = bytes.fromhex('32510ba9a7b2bba9b8005d43a304b5714cc0bb0c8a34884dd91304b8ad40b62b07df44ba6e9d8a2368e51d04e0e7b207b70b9b8261112bacb6c866a232dfe257527dc29398f5f3251a0d47e503c66e935de81230b59b7afb5f41afa8d661cb')
msg_4 = bytes.fromhex('32510ba9aab2a8a4fd06414fb517b5605cc0aa0dc91a8908c2064ba8ad5ea06a029056f47a8ad3306ef5021eafe1ac01a81197847a5c68a1b78769a37bc8f4575432c198ccb4ef63590256e305cd3a9544ee4160ead45aef520489e7da7d835402bca670bda8eb775200b8dabbba246b130f040d8ec6447e2c767f3d30ed81ea2e4c1404e1315a1010e7229be6636aaa')
msg_5 = bytes.fromhex('3f561ba9adb4b6ebec54424ba317b564418fac0dd35f8c08d31a1fe9e24fe56808c213f17c81d9607cee021dafe1e001b21ade877a5e68bea88d61b93ac5ee0d562e8e9582f5ef375f0a4ae20ed86e935de81230b59b73fb4302cd95d770c65b40aaa065f2a5e33a5a0bb5dcaba43722130f042f8ec85b7c2070')
msg_6 = bytes.fromhex('32510bfbacfbb9befd54415da243e1695ecabd58c519cd4bd2061bbde24eb76a19d84aba34d8de287be84d07e7e9a30ee714979c7e1123a8bd9822a33ecaf512472e8e8f8db3f9635c1949e640c621854eba0d79eccf52ff111284b4cc61d11902aebc66f2b2e436434eacc0aba938220b084800c2ca4e693522643573b2c4ce35050b0cf774201f0fe52ac9f26d71b6cf61a711cc229f77ace7aa88a2f19983122b11be87a59c355d25f8e4')
msg_7 = bytes.fromhex('32510bfbacfbb9befd54415da243e1695ecabd58c519cd4bd90f1fa6ea5ba47b01c909ba7696cf606ef40c04afe1ac0aa8148dd066592ded9f8774b529c7ea125d298e8883f5e9305f4b44f915cb2bd05af51373fd9b4af511039fa2d96f83414aaaf261bda2e97b170fb5cce2a53e675c154c0d9681596934777e2275b381ce2e40582afe67650b13e72287ff2270abcf73bb028932836fbdecfecee0a3b894473c1bbeb6b4913a536ce4f9b13f1efff71ea313c8661dd9a4ce')
msg_8 = bytes.fromhex('315c4eeaa8b5f8bffd11155ea506b56041c6a00c8a08854dd21a4bbde54ce56801d943ba708b8a3574f40c00fff9e00fa1439fd0654327a3bfc860b92f89ee04132ecb9298f5fd2d5e4b45e40ecc3b9d59e9417df7c95bba410e9aa2ca24c5474da2f276baa3ac325918b2daada43d6712150441c2e04f6565517f317da9d3')
msg_9 = bytes.fromhex('271946f9bbb2aeadec111841a81abc300ecaa01bd8069d5cc91005e9fe4aad6e04d513e96d99de2569bc5e50eeeca709b50a8a987f4264edb6896fb537d0a716132ddc938fb0f836480e06ed0fcd6e9759f40462f9cf57f4564186a2c1778f1543efa270bda5e933421cbe88a4a52222190f471e9bd15f652b653b7071aec59a2705081ffe72651d08f822c9ed6d76e48b63ab15d0208573a7eef027')
msg_10 = bytes.fromhex('466d06ece998b7a2fb1d464fed2ced7641ddaa3cc31c9941cf110abbf409ed39598005b3399ccfafb61d0315fca0a314be138a9f32503bedac8067f03adbf3575c3b8edc9ba7f537530541ab0f9f3cd04ff50d66f1d559ba520e89a2cb2a83')

msg_target = bytes.fromhex('32510ba9babebbbefd001547a810e67149caee11d945cd7fc81a05e9f85aac650e9052ba6a8cd8257bf14d13e6f0a803b54fde9e77472dbff89d71b57bddef121336cb85ccb8f3315f4b52e301d16e9f52f904')

msgs = [msg_1, msg_2, msg_3, msg_4, msg_5, msg_6, msg_7, msg_8, msg_9, msg_10]

def bytesxor(a, b):
    if len(a) > len(b):
       return bytes([x ^ y for (x, y) in zip(a[:len(b)], b)])
    else:
       return bytes([x ^ y for (x, y) in zip(a, b[:len(a)])])

根据前文理论,我们先尝试异或 msg_1msg_2,结果是

b'\x12\x10L\x06\x13NW\t\x14\x0f\x10O\x02R\x1b\n\x04B\x02\x0cM\x07\x0b\x18OH\x15T\x1f\x08\x00HN\x1e\x02A\x06\x1d\x06MT\x0b\n\x02\x02\x10\x19E\x10\x16MO:\x00SC\x00NC\x0e\x1e\x1d\nRF\x12\x17\x1b\x01\x17\x00\x1b\x0eEC\x1c\x0c\x1d\x16\nR\r\x11tN\x19\x06\x1a\x11M\x0eU\x17O\x08NT7\x14\x05\x0b\x17CST\x1bH\x07\x0e\x00\x0eM'

我们不使用十六进制格式输出,是因为需要观察其中有一些大写或小写英文字母。根据题目,明文是 ASCII 英语句子,其中大部分符号都是大小写英文字母和空格。我们需要知道一个特殊的规律

$$ ASCII(大/小写字母)\oplus ASCII(空格)=ASCII(小/大写字母) $$

英文字母的 ASCII 值与空格的 ASCII 值异或得到的 ASCII 值相当于切换原字母的大小写

根据以上规律,我们十个中的一个密文 C,与其余九个密文逐字节异或,得到九个字节流 B_i,观察其中相同索引 j(位置)的字节 b_k,如果这九个字节几乎都是英文字母,那么我们选取的密文 C 的索引 j 处的字节 C[j] 的明文就大概是空格。此时我们有

$$ C[j]\oplus ASCII(空格)=K[j] $$

从而有可能还原出一个字节的密钥。如果密文足够多足够长,重复上述过程可以还原完整的密钥。

key = [0] * len(msg_7) # `msg_7` is the longest ciphertext.

def isalpha(b):
    return (ord('a') <= b <= ord('z')) or (ord('A') <= b <= ord('Z'))

for i, msg_i in enumerate(msgs):
    may_not_space = [0] * len(msg_i) # Count cases when b_k is not alphabetic.
    for j, msg_j in enumerate(msgs):
        if i != j:
            xored = bytesxor(msg_i, msg_j)
            for k, xb in enumerate(xored):
                if (not isalpha(xb)) and xb != 0:
                    may_not_space[k] += 1
    
    for j, may_not in enumerate(may_not_space):
        if may_not <= 2: # If almost all b_k are alphabetic
            key_byte = msg_i[j] ^ ord(' ')
            if key[j] == 0:
                key[j] = key_byte
                continue
            
            if key[j] != key_byte: # Detect contradiction. Do more checks.
                reliable = True
                for m in msgs:
                    if j >= len(m):
                        continue
                    byte = m[j] ^ key_byte
                    if not isalpha(byte) and byte != ord(' '):
                        reliable = False
                        break
                if reliable:
                    key[j] = key_byte

print(f'Recovered key: {key}')
print(f'Target message: {bytesxor(bytes(key), msg_target)}'')
# for msg in msgs:
#     print(bytesxor(msg, bytes(key)))

题目二

地址:https://www.cryptopals.com/sets/1

1. Convert hex to base64

from base64 import b64encode

print(b64encode(bytes.fromhex('49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d')).decode())

2. Fixed XOR

def bytesxor(a, b):
    if len(a) > len(b):
       return bytes([x ^ y for (x, y) in zip(a[:len(b)], b)])
    else:
       return bytes([x ^ y for (x, y) in zip(a, b[:len(a)])])

print(bytesxor(bytes.fromhex('1c0111001f010100061a024b53535009181c'), bytes.fromhex('686974207468652062756c6c277320657965')).hex())

3. Single-byte XOR cipher

突发奇想,这里我用《动物森友会》给信件评分的算法来判断哪个结果最正确。(还真没毛病

def get_score(message: str) -> int:
    """*Animal Crossing* message scoring algorithm."""
    t = set("abl abo abr abs acc ach acr act add adm adv aer aff afr aft aga age ago ahe air ali all alm alo alr als alt alw am  ame amo and ang ani ano ans any apa app apr are arg arm arr art asi ask asl ate atm att aud aug aut ave avo awa cak cal cam can cap car cas cat cau cen cer cha che chi cho chi chu cir cit cla cle cli clo coa cof coi col com con coo cop cor cos cou cov cow cre cri cro cry cup cur cus cut bab bac bad bag bal ban bas bat be  bea bec bed bee bef beg beh bel bes bet bey bic big bik bil bir bit bla ble blo blu boa bod bon boo bor bot bou box boy bra bre bri bro bui bur bus but buy by  eac ear eas eat edu eff egg eig eit ele els emp end ene eng enj eno ent equ err esp eur eve exa exc exe exp eye dad dai dam dan dar dat dau day dea dec dee def deg del dem den dep des det dev dic did die dif dig din dir dis div do  doc doe dog dol don doo dou dow doz dra dre dri dro dru dry due dur dus dut gai gam gar gas gat gav gen ger get gir giv gla go  god goi gon goo got gov gra gre gro gua gue gui gun fac fai fal fam far fas fat fea feb fed fee fel few fie fif fig fil fin fir fis fiv fix fla fle fli flo fly fol foo for fou fra fre fri fro fru ful fun fut i   ice ide if  ima imm imp in  inc ind inf ins int inv iro is  isl it  its hab had hai hal han hap har has hat hav he  hea hei hel her hi  hid hig hil him hir his hit hol hom hon hop hor hos hot hou how hum hun hur hus kee kep key kic kil kin kit kne kni kno kab kad kai kak kan kar kas kat kau kaw kay kaz kea ked kef keg ken kes ket kev kib kie kif kig kik kim kin kis kit kiv koc kon koo kos kot kou kov kow kun kyi kac kad kag kai kaj kak kan kap kar kat kay ke  kea ked kee kem ken kes ket kid kig kil kin kis kod kom kon koo kor kos kot kou kov kuc kum kus ky  kys kam kar kat kea kec kee kei kev kew kex kic kig kin ko  kob koi kon koo kor kos kot kov kow kum kbj k'c kct kf  kff kft kh  kil kka kld kn  knc kne knl kpe kpi kpp kr  kra krd kth kur kut kve kwn jan jap job joi jud jul jum jun jus qua que qui pac pag pai pap par pas pat pay pea pen peo per pho pic pie pin pip pla ple poc poi pol poo pop pos pot pou pow pra pre pri pro pub pul pup pur pus put sad saf sai sal sam san sat sav saw say sce sch sci sco sea sec see sel sen sep ser set sev sex sha she shi sho shu sic sid sig sil sim sin sis sit six siz ski sky sle sli slo sma sme smi smo sno so  soa soc sof soi sol som son soo sor sou spa spe spi spo spr squ sta ste sti sto str stu sty sub suc sud suf sug sum sun sup sur swa swe swi swu sys rac rad rai ran rap rat rea rec red ref reg rel rem rep req res ret ric rid rig rin ris riv roa roc rod rol roo ros rou row rul run rus una unc und uni unl unt up  upo us  use usu tab tak tal tas tau tax tea tee tel tem ten ter tes tha the thi tho thr thu tic tie til tim tir tit to  tod tog tol tom ton too top tor tot tou tow tra tre tri tro tru try tue tur tv  twe twi two tyi typ val var veg ver vie vil vis voi vol vot vai vak val van var vas vat vav vay ve  vea ved vee vei vel ven ver ves vet vha vhe vhi vho vhy vid vif vil vin vir vis vit viv vok vom von voo vor vou vri vro vma yar yea yel yen yes yet you zer".split())
    s = 0
    if message and message[-1] in '.?!':
        s += 20
    for i, c in enumerate(message):
        if c in '.?!':
            for j in range(i+1, min(i+4, len(message))):
                if message[j].isupper():
                    s += 10
                    break
                elif message[j].isalpha():
                    s -= 10
                    break
    s += sum(3 for w in message.split() if len(cw :=
             ''.join(c for c in w if c.isalpha()).lower()) >= 3 and cw[:3] in t)
    for c in message:
        if not c.isspace():
            s += 20 if c.isupper() else -10
            break
    for i in range(len(message)-2):
        if message[i].isalpha() and message[i] == message[i+1] == message[i+2]:
            s -= 50
            break
    sp, nsp = message.count(' '), len(message) - message.count(' ')
    s += -20 if nsp == 0 or (sp * 100 // nsp if nsp else 0) < 20 else 20
    if len(message) > 75:
        c = 0
        for ch in message:
            c = 0 if ch in '.?!' else c + 1
            if c == 75:
                s -= 150
                break
    s -= sum(20 for i in range(0, len(message), 32)
             if ' ' not in message[i:i+32] and len(message[i:i+32]) == 32)
    return s


cipher = bytes.fromhex(
    '1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736')
results = list()
for c in range(256):
    try:
        res = bytesxor((chr(c) * len(cipher)).encode(), cipher).decode()
        results.append((res, get_score(res)))
    except UnicodeDecodeError:
        pass

best = sorted(results, key=lambda x: x[1])[-1][0]
print(best)

# Cooking MC's like a pound of bacon

4. Detect single-character XOR

with open('4.txt', 'r') as file:
    ciphers = file.readlines()

for original_cipher in ciphers:
    results = list()
    cipher = bytes.fromhex(original_cipher)
    for c in range(256):
        try:
            res = bytesxor((chr(c) * len(cipher)).encode(), cipher).decode()
            results.append((res, get_score(res)))
        except UnicodeDecodeError:
            pass
    if len(results) == 0:
        continue

    best = sorted(results, key=lambda x: x[1])[-1]
    if best[1] > 40:
        print(f'{original_cipher.strip()} -> {best[0]}')

# 7b5a4215415d544115415d5015455447414c155c46155f4058455c5b523f -> Now that the party is jumping

5. Implement repeating-key XOR

def repeating_key_xor_to_hex(msg: bytes, key: bytes) -> str:
    result_chars = []
    keylen = len(key)
    for i, b in enumerate(msg):
        result_chars.append(b ^ key[i % keylen])
    return bytes(result_chars).hex()

print(repeating_key_xor_to_hex(b"Burning 'em, if you ain't quick and nimble", b'ICE'))
print(repeating_key_xor_to_hex(b'I go crazy when I hear a cymbal', b'ICE'))

6. Break repeating-key XOR

一开始没有还原正确,检查了一下发现是 # 扰乱了解密评分。考虑到英文句子里极少有 #,所以遇到就扣 5 分。

from base64 import b64decode


def get_score(message: str):
    score = 0
    for c in message:
        if c.islower():
            score += 3
        if c.isupper():
            score += 1
        if c == ' ':
            score += 1
        if c == '#':
            score -= 5
    return score


def get_key(cipher: bytes) -> int:
    results = list()
    for c in range(256):
        try:
            res = bytesxor((chr(c) * len(cipher)).encode(), cipher).decode()
            results.append((c, get_score(res)))
        except UnicodeDecodeError:
            pass
    return sorted(results, key=lambda x: x[1])[-1][0]


def repeating_key_xor(msg: bytes, key: bytes) -> bytes:
    result_chars = []
    keylen = len(key)
    for i, b in enumerate(msg):
        result_chars.append(b ^ key[i % keylen])
    return bytes(result_chars)


def normalized_average_hd(data: bytes, unit_len: int) -> float:
    chunks = [data[i:i + unit_len] for i in range(0, len(data), unit_len)]
    num_chunks = len(chunks)
    total_hd = 0
    count = 0
    for i in range(num_chunks):
        for j in range(i + 1, num_chunks):
            if len(chunks[i]) == unit_len and len(chunks[j]) == unit_len:
                total_hd += sum((byte1 ^ byte2).bit_count()
                                for byte1, byte2 in zip(chunks[i], chunks[j]))
                count += 1
    return total_hd / (count * unit_len)


with open('6.txt', 'r') as file:
    cipher = b64decode(file.read())

results = list()
for l in range(2, 41):
    norm_hd = normalized_average_hd(cipher, l)
    results.append((l, norm_hd))
keylens = sorted(results, key=lambda x: x[1])

for k in range(1):
    keylen = keylens[k][0]
    print(f"Guessed key length: {keylen}")

    key_bytes = []
    for i in range(keylen):
        block = bytes([cipher[j * keylen + i]
                      for j in range(len(cipher) // keylen)])
        key_bytes.append(get_key(block))

    key = bytes(key_bytes)
    # print(repeating_key_xor(cipher, key).decode())
    print(f'Key: {key}')

# Guessed key length: 29
# Key: b'Terminator X: Bring the noise'

7. AES in ECB mode

import base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms

key = b"YELLOW SUBMARINE"
with open('7.txt', 'r') as f:
    encrypted_b64 = f.read()
encrypted_data = base64.b64decode(encrypted_b64)
cipher = Cipher(algorithms.AES(key)) # Default to ECB mode
decryptor = cipher.decryptor()
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()

print(decrypted_data.decode())

8. Detect AES in ECB mode

print(max(open('8.txt').read().splitlines(), key=lambda x: len(c:=[x[i:i+32] for i in range(0, len(x), 32)]) - len(set(c))))