TSG CTF 2021 writeup

Writer:b1uef0x / Webページ建造途中

概要

チーム参加。初めて参加した昨年は1完も届かなかったが、今年は簡単なCryptoを解いた。Webのエスパー力が高すぎて断念したのが心残り。

目次

Beginner's Crypto 2021 (Crypto)

c1「うっす、よろしく。」
c2「がんばります、よろしく。」
c3「よっす、どうも。」

初心者向けヒント:

暗号化を行うPythonスクリプトと暗号化出力が配布される。

beginners_crypto_2021.pyfrom secret import e
from Crypto.Util.number import getStrongPrime, isPrime

p = getStrongPrime(1024)
q = getStrongPrime(1024)
N = p * q
phi = (p - 1) * (q - 1)

with open('flag.txt', 'rb') as f:
    flag = int.from_bytes(f.read(), 'big')

assert(isPrime(e))
assert(isPrime(e + 2))
assert(isPrime(e + 4))

e1 = pow(e, 0x10001, phi)
e2 = pow(e + 2, 0x10001, phi)
e3 = pow(e + 4, 0x10001, phi)

c1 = pow(flag, e1, N)
c2 = pow(flag, e2, N)
c3 = pow(flag, e3, N)

print(f'p = {p}')
print(f'q = {q}')
print(f'c1 = {c1}')
print(f'c2 = {c2}')
print(f'c3 = {c3}')

未知のeが読み込まれているが、e、e+2、e+4がすべて素数となることからeは三つ子素数である。この形式の三つ子素数は3,5,7しか存在しないため、e=3となる。

eがわかったことから、e1~e3までがわかる。今回は異なるeで同一の平文を暗号化した問題として、Common Modulus Attackで解いてみる。

plain RSAに対する攻撃手法を実装してみる - ももいろテクノロジー( https://inaz2.hatenablog.com/entry/2016/01/15/011138 )から拾ってきたスクリプトを使って以下のsolverを作成。

import gmpy2
from Crypto.Util.number import bytes_to_long, long_to_bytes

def common_modulus_attack(c1, c2, e1, e2, n):
    gcd, s1, s2 = gmpy2.gcdext(e1, e2)
    if s1 < 0:
        s1 = -s1
        c1 = gmpy2.invert(c1, n)
    elif s2 < 0:
        s2 = -s2
        c2 = gmpy2.invert(c2, n)
    v = pow(c1, s1, n)
    w = pow(c2, s2, n)
    m = (v * w) % n
    return m


n = 21861849068488503326777564160482917768295313612622098844005843941150416001449415831637974753612296634178937206635448545759776608072919261837704109903460487852321593647461325194355863907156869648747188133965073402040821653255878590888415203522221544646026084652799982648089185458004128592803363049028863875147302298032914963095395238481777617108444373792690712586751618914946813447656100899378283397273293143842550921101844777123910587444282360703980995288785551256271451760332791447461673193550567714926450717517656392539447836755985235708569019532947491148812339977048504141761177263545250566883133462383528417086483
e1 = 17603549263186425341767929511665789959105944330977732610916014873119252282802636800619943218173617441888097241786330044467390706899621342311537780960325827434196373002926945337049116646768167280435578161313136166984912128140809491969353494782750474301810262688555936515237213157072589888855009082917459841124317212785314003914313234917639294833196462895550624990993855672424098551845194332818039618333455201098641549770441114401683833649029496333471721218422923953911572405255917219368996940141028493863221349007615701640977231767979869511746450459020309485973427107977390630775364092661966159756765434661573566462163
e2 = 15344508209087520408904622039612403292778077018374460918944647544769165640337137582186403919710159968904923421386737007919512784139878000070417785469235072760767520470631932153936681007172335035560603688152851363757551810390163356371161738851859293395985292893266642634947470905628261585873301406883399307465584080339646976686630070325570603080476048485894627862169328144852580941394727582550746753409269261023938462291893093488806622999431476759128322955553737212823603838438441286632412590780859732560765318694595239577819399088507513384628815021733400052028849608900698718593489414974683054465037065557915104359925

c1 = 2560344169447809042170685026483682125499025654554670516499742981486615082413150123244985585751880264831112089324011804397189638172356179296987581738515619297036118472798499254785110885662931526277474101787493114656242031264678448394380651657330967744585361662315313462698221954777506355498445242300193032704972074020068699180111637362566860530694807230108024167631423062629721393506643291591971626450262144814424411172618188943774725105690851574922374544865628890948773274109561622040022136970632948166009941425683576381155722191980954262373394704682297682490061906408535261437100820855976015526295573831744458528440
c2 = 9041231631916227099296501948589424780380702196870972231114747229225732542137483840187783630590878594711315671224997985975031038623195921968945234067183003568830416719957054703139219879265482072634572699299971785171441858501409377942183918216246312330291820452436486171483461790388518159980027140392750222843449604265528929311978655519463562520038992870162220913137870017065557254099767583925177889051326144499369420594398043223307161794788085369471538477803421726790780799629276012701406231535048423554314287152404245482928538931953627397633165453319078105028671410039195670727134471011040601278722143504641171853743

print(long_to_bytes(common_modulus_attack(c1, c2, e1, e2, n)))

eとcのペアは3つあるが、2つあれば復号できる。実行するとflagを入手。

TSGCTF{You are intuitively understanding the distribution of prime numbers! Bonus: You can solve this challenge w/ N instead of p and q!}

B??e64 (Misc)

デコードするだけだとつまらないと思うので,自主的に一部を隠してデコードしてみせてください.

この問題は数回程度のサーバ接続で解けることが想定されています.ルールに記載の通り,DoS に相当する行為はおやめください.

nc 34.146.212.53 9999

解けなかった問題。solverまでは完成したがいい感じのmaskを作れなかったので、進んだところまでまとめることにする。スクリプトはあまり整理されていない。

ncでサーバーに接続して解く問題。サーバーで動作しているスクリプトが配布される。

import random
import string
import base64
import os


def gen_chal():
    cs = string.ascii_lowercase + string.digits
    return ''.join(random.choices(cs, k=32))


def enc(s, encoding='ascii'):
    for _ in range(8):
        s = base64.b64encode(bytes(s, encoding)).decode(encoding)
    return s


def main():
    # receive a mask
    mask = input('mask: ')
    if len(mask) != 344 or any(c not in '*?' for c in mask):
        print('bad mask :(')
        return
    if mask.count('?') > 124:
        print("don't be too greedy!")
        return

    for q in range(1, 9):
        # generate a challenge
        secret = gen_chal()
        encoded = enc(secret)
        assert len(encoded) == 344

        # give them the masked secret
        masked = ''.join('*' if m == '*' else c for m, c in zip(mask, encoded))
        print(f'Q{q}: {masked}')

        # let them guess the secret
        guess = input(f'A{q}: ')
        if secret != guess:
            print('you lose!')
            print(f'secret: {secret}')
            return
        else:
            print("correct!")

    print('you win!')
    print(f"flag: {os.environ.get('FLAG')}")


if __name__ == '__main__':
    main()

ローカルで動作させると以下のようになる。

スクリプトの動作

344文字のmask(隠すところが*、表示するところが?)を入力すると、32文字の半角英数字を8回base64エンコードしたデータがマスクされた状態で表示される。マスクされたデータから元の32文字の半角英数字を見つけ出して8回連続で正解すればflagが表示される。

作戦は色々と考えられるが、32文字の元データを344文字に膨張させていることから、断片からでも全体を復元できるような情報が乗っていると考え、かつマスクされたbase64を逆回しにすることは難しいため、エンコード後の文字列とエンコード前の文字列を対応付けるためのデータベースを作ってマッチさせていく作戦を考えた。

例えば、任意の3文字から始まる文字列について、それ以外の29文字をランダムに変化させて100個ずつエンコードして共通する部分以外をマスクした例が以下のものになる。

tad : Vm0xd1MwMUdXWGxTV0doVVYwZDRWRll3Wk********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tae : Vm0xd1MwMUdXWGxTV0doVVYwZDRWVmxVU*********************************************************************J*************************V*******S*********************************************N*************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
taf : Vm0xd1MwMUdXWGxTV0doVVYwZDRWVmxy**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tag : Vm0xd1MwMUdXWGxTV0doVVYwZDRWVmx0**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tah : Vm0xd1MwMUdXWGxTV0doVVYwZDRWVll3Wk********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tai : Vm0xd1MwMUdXWGxTV0doVVYwZDRWbGxVU*********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
taj : Vm0xd1MwMUdXWGxTV0doVVYwZDRWbGxy**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tak : Vm0xd1MwMUdXWGxTV0doVVYwZDRWbGx0**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tal : Vm0xd1MwMUdXWGxTV0doVVYwZDRWbFl3Wk********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tam : Vm0xd1MwMUdXWGxTV0doVVYwZDRWMWxVU*********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tan : Vm0xd1MwMUdXWGxTV0doVVYwZDRWMWxy**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tao : Vm0xd1MwMUdXWGxTV0doVVYwZDRWMWx0**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tap : Vm0xd1MwMUdXWGxTV0doVVYwZDRWMVl3Wk********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
taq : Vm0xd1MwMUdXWGxTV0doVVYwZDRXRmxVU*********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tar : Vm0xd1MwMUdXWGxTV0doVVYwZDRXRmxy**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tas : Vm0xd1MwMUdXWGxTV0doVVYwZDRXRmx0**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tat : Vm0xd1MwMUdXWGxTV0doVVYwZFNUMVpz**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tau : Vm0xd1MwMUdXWGxTV0doVVYwZFNUMVp0**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tav : Vm0xd1MwMUdXWGxTV0doVVYwZFNUMVl3V*********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
taw : Vm0xd1MwMUdXWGxTV0doVVYwZFNUMVV3Wk********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tax : Vm0xd1MwMUdXWGxTV0doVVYwZFNVRlpz**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
tay : Vm0xd1MwMUdXWGxTV0doVVYwZFNVRlp0**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
taz : Vm0xd1MwMUdXWGxTV0doVVYwZFNVRll3V*********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
ta0 : Vm0xd1MwMUdXWGxTV0doVVlteEtWMVl3Wk********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
ta1 : Vm0xd1MwMUdXWGxTV0doVVlteEtXRmxVU*********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
ta2 : Vm0xd1MwMUdXWGxTV0doVVlteEtXRmxy**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
ta3 : Vm0xd1MwMUdXWGxTV0doVVlteEtXRmx0**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
ta4 : Vm0xd1MwMUdXWGxTV0doVVltdHdUMVpz**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==
ta5 : Vm0xd1MwMUdXWGxTV0doVVltdHdUMVp0**********************************************************************J*************************V*******S***********************************************************************************************************************V***********************************************V************************nNTbGRTTTAwMQ==

1文字を固定して31文字をランダムにする場合には特徴が埋もれてしまうが、ある文字の前後1文字ずつの3文字としたときにはそれ以外の文字がどんなものでも変化しない部分を作ることができる。

よって、N文字目とその前後1文字ずつの3文字を固定したとき、必ず同一になるエンコード文字列のデータベースを作り、これを1~32文字目についてすべてデータベース化することで、任意のエンコード文字列が与えられたとき、各桁ごとにそのエンコード文字列の部分を作りうる元の文字列の候補を得ることができるはずだ。

まず第一段階として、N番目を中心とする[N-1][N][N+1]の3文字を固定したときにエンコード文字列のどの部分が固定されるかを調査する。

次のプログラムを書いて、エンコード文字列が不変となるオフセット位置を計算した。任意の3文字(36×36×36)について100回ずつエンコード文字列を計算し、36×36×36×100個のデータの共通区間を探し出す。これを1~32文字目までの32パターン実行する。

import random
import string
import base64
import os
import re

cs = string.ascii_lowercase + string.digits

def gen_chal():
    return ''.join(random.choices(cs, k=32))

def gen_chal5(forward,char,back,pos):
    if pos==0:
        return char + back + ''.join(random.choices(cs, k=30-pos))
    elif pos==31:
        return ''.join(random.choices(cs, k=pos-1)) + forward + char
    else : 
        return ''.join(random.choices(cs, k=pos-1)) + forward + char + back + ''.join(random.choices(cs, k=30-pos))


def enc(s, encoding='ascii'):
    for _ in range(8):
        s = base64.b64encode(bytes(s, encoding)).decode(encoding)
    return s


_max = 344
_min = 0
pos = 0
for char in cs:
        for j in cs:
                d = "*"+char+j
                strs = list(enc(gen_chal5("",char,j,pos)))
                for k in range(100):
                        encoded = list(enc(gen_chal5("",char,j,pos)))
                        for l in range(len(strs)):
                                if strs[l]!="*" and strs[l]!=encoded[l]:
                                        strs[l] = "*"
                strss = "".join(strs)
                m = re.search('[a-zA-Z0-9]{10,}', strss)
                _max = m.end() if _max>m.end() else _max
                _min = m.start() if _min<m.start() else _min
print("offsets += [[" + str(_max) +","+str(_min)+"]]#"+str(pos))


for pos in range(1,31,1):
        _max = 344
        _min = 0
        for char in cs:
                for i in cs:
                        for j in cs:
                                d = i+char+j
                                strs = list(enc(gen_chal5(i,char,j,pos)))
                                for k in range(100):
                                        encoded = list(enc(gen_chal5(i,char,j,pos)))
                                        for l in range(len(strs)):
                                                if strs[l]!="*" and strs[l]!=encoded[l]:
                                                        strs[l] = "*"
                                strss = "".join(strs)
                                m = re.search('[a-zA-Z0-9]{10,}', strss)
                                _max = m.end() if _max>m.end() else _max
                                _min = m.start() if _min<m.start() else _min
        print("offsets += [[" + str(_max) +","+str(_min)+"]]#"+str(pos))

pos = 31
_max = 344
_min = 0
for char in cs:
        for i in cs:
                d = i+char+"*"
                strs = list(enc(gen_chal5(i,char,"",pos)))
                for k in range(100):
                        encoded = list(enc(gen_chal5(i,char,"",pos)))
                        for l in range(len(strs)):
                                if strs[l]!="*" and strs[l]!=encoded[l]:
                                        strs[l] = "*"
                strss = "".join(strs)
                m = re.search('[a-zA-Z0-9]{10,}', strss)
                _max = m.end() if _max>m.end() else _max
                _min = m.start() if _min<m.start() else _min
print("offsets += [[" + str(_max) +","+str(_min)+"]]#"+str(pos))

出力は次のプログラムのためにPythonコードとして出力される。

offsets += [[18,0]]#0
offsets += [[32,0]]#1
offsets += [[37,20]]#2
offsets += [[48,27]]#3
offsets += [[64,36]]#4
offsets += [[70,48]]#5
offsets += [[77,63]]#6
offsets += [[93,64]]#7
offsets += [[98,84]]#8
offsets += [[107,86]]#9
offsets += [[122,95]]#10
offsets += [[127,112]]#11
offsets += [[137,115]]#12
offsets += [[153,128]]#13
offsets += [[157,139]]#14
offsets += [[168,150]]#15
offsets += [[183,154]]#16
offsets += [[187,171]]#17
offsets += [[195,179]]#18
offsets += [[210,186]]#19
offsets += [[218,200]]#20
offsets += [[225,206]]#21
offsets += [[243,218]]#22
offsets += [[250,228]]#23
offsets += [[257,239]]#24
offsets += [[273,248]]#25
offsets += [[277,264]]#26
offsets += [[288,267]]#27
offsets += [[300,275]]#28
offsets += [[307,291]]#29
offsets += [[342,299]]#30
offsets += [[342,304]]#31

切り出すべき文字列の区間がわかった。なお前後の文字を含めてが3文字としているため、0番目(先頭)と1番目は重複し、30番目と31番目(終端)も重複する。そのためオフセット位置も重複しており、実際の計算は1番目~30番目の30文字に対して行う。

次に実際にN番目の3文字のデータベースを作る。N番目について、[ランダムな文字列][N-1][N][N+1][ランダムな文字列]のエンコード文字列を切り出したデータと[N-1][N][N+1]を対応付ける。

例えば0から数えて10番目の文字を中心とする3文字の元文字列とエンコード文字列のリストの一部は次のようになる。

HVm5OaFJtaG9UVlp3ZVZkV1ZtRl:aaa,aab
HVm5OaFJtaG9UVlp3ZVZkV1ZtdF:aac
HVm5OaFJtaG9UVlp3ZVZkV1ZsWm:aad
HVm5OaFJtaG9UVlp3ZVZkV1dtRl:aae,aaf
HVm5OaFJtaG9UVlp3ZVZkV1dtdF:aag
HVm5OaFJtaG9UVlp3ZVZkV1dsWm:aah
HVm5OaFJtaG9UVlp3ZVZkV1pEUl:aai,aaj
HVm5OaFJtaG9UVlp3ZVZkV1kzaF:aak
HVm5OaFJtaG9UVlp3ZVZkV1pIcG:aal
HVm5OaFJtaG9UVlp3ZVZkV1VrZF:aam,aan
HVm5OaFJtaG9UVlp3ZVZkV1VrdF:aao
HVm5OaFJtaG9UVlp3ZVZkV1VrSm:aap
HVm5OaFJtaG9UVlp3ZVZkWGRHRl:aaq,aar
HVm5OaFJtaG9UVlp3ZVZkWGRHdF:aas
HVm5OaFJtaG9UVlp3ZVZaclpEUl:aat,aau
HVm5OaFJtaG9UVlp3ZVZaclkzaF:aav
HVm5OaFJtaG9UVlp3ZVZaclpIcG:aaw
HVm5OaFJtaG9UVlp3ZVZaclVrZF:aax,aay
HVm5OaFJtaG9UVlp3ZVZaclVrdF:aaz
HVm5OaFJtaG9UVlp3YjFkV1VrSm:aa0
HVm5OaFJtaG9UVlp3YjFkWGRHRl:aa1,aa2
HVm5OaFJtaG9UVlp3YjFkWGRHdF:aa3
HVm5OaFJtaG9UVlp3YjFaclpEUl:aa4,aa5
HVm5OaFJtaG9UVlp3YjFaclkzaF:aa6
HVm5OaFJtaG9UVlp3YjFaclpIcG:aa7
HVm5OaFJtaG9UVlp3YjFaclVrZF:aa8,aa9
HVm5OaVJsWlhZa1p3ZVZkV1ZtRl:baa,bab
HVm5OaVJsWlhZa1p3ZVZkV1ZtdF:bac
HVm5OaVJsWlhZa1p3ZVZkV1ZsWm:bad
HVm5OaVJsWlhZa1p3ZVZkV1dtRl:bae,baf
HVm5OaVJsWlhZa1p3ZVZkV1dtdF:bag
HVm5OaVJsWlhZa1p3ZVZkV1dsWm:bah
HVm5OaVJsWlhZa1p3ZVZkV1pEUl:bai,baj
HVm5OaVJsWlhZa1p3ZVZkV1kzaF:bak
HVm5OaVJsWlhZa1p3ZVZkV1pIcG:bal
HVm5OaVJsWlhZa1p3ZVZkV1VrZF:bam,ban

このようなデータベースを作ると、切り出したエンコード文字列からその位置にある3文字の候補を得ることができるようになる。次のプログラムを書いてデータベースを作成してdb.txtに保存している。

import random
import string
import base64
import os
import pickle

cs = string.ascii_lowercase + string.digits



def gen_chal():
    cs = string.ascii_lowercase + string.digits
    return ''.join(random.choices(cs, k=32))

def gen_chal5(forward,char,back,pos):
    if pos==0:
        return char + back + ''.join(random.choices(cs, k=30-pos))
    elif pos==31:
        return ''.join(random.choices(cs, k=pos-1)) + forward + char
    else : 
        return ''.join(random.choices(cs, k=pos-1)) + forward + char + back + ''.join(random.choices(cs, k=30-pos))

def enc(s, encoding='ascii'):
    for _ in range(8):
        s = base64.b64encode(bytes(s, encoding)).decode(encoding)
    return s


offsets = []
offsets += [[18,0]]#0
offsets += [[32,0]]#1
offsets += [[37,20]]#2
offsets += [[48,27]]#3
offsets += [[64,36]]#4
offsets += [[70,48]]#5
offsets += [[77,63]]#6
offsets += [[93,64]]#7
offsets += [[98,84]]#8
offsets += [[107,86]]#9
offsets += [[122,95]]#10
offsets += [[127,112]]#11
offsets += [[137,115]]#12
offsets += [[153,128]]#13
offsets += [[157,139]]#14
offsets += [[168,150]]#15
offsets += [[183,154]]#16
offsets += [[187,171]]#17
offsets += [[195,179]]#18
offsets += [[210,186]]#19
offsets += [[218,200]]#20
offsets += [[225,206]]#21
offsets += [[243,218]]#22
offsets += [[250,228]]#23
offsets += [[257,239]]#24
offsets += [[273,248]]#25
offsets += [[277,264]]#26
offsets += [[288,267]]#27
offsets += [[300,275]]#28
offsets += [[307,291]]#29
offsets += [[342,299]]#30
offsets += [[342,304]]#31

db = []
for i in range(32):
        db += [{}]

#create database
for pos in range(1,31,1):
        for char in cs:
                for i in cs:
                        for j in cs:
                                d = i+char+j
                                offset = offsets[pos]
                                strs = enc(gen_chal5(i,char,j,pos))[offset[1]:offset[0]]
                                if strs not in db[pos]:
                                        db[pos][strs] = []
                                db[pos][strs] += [d]
        print(pos)

f = open('db.txt', 'wb')
pickle.dump(db, f)
f.close()

データベースが完成したので、このデータベースを利用してエンコード文字列から元の文字列を得るコードを作る。入力されたエンコード文字列を各桁ごとにoffset位置で区切って、その文字列のkeyを持つ辞書配列を参照して元の字列の候補を得る。これらの候補は前後1文字を含んだものになっているので、隣の桁の候補同士を比較して文字が一致する候補以外を削除すれば絞り込むことができる。

例えばN文字目の候補がabc,defの2つ、N+1文字目の候補がefg,fghの2つなら、N文字目の候補の2,3文字目とN+1文字目の1,2文字目が共通する候補を絞り込めばよい。この場合、defとefgが残るので、N文字目がe、N+1文字目がfになる。

次のプログラムで文字列を復元する。まずmaskなしのエンコード文字列を100個渡して元の文字列を復元できるか試している。

import random
import string
import base64
import os
import pickle

cs = string.ascii_lowercase + string.digits

def gen_chal():
    cs = string.ascii_lowercase + string.digits
    return ''.join(random.choices(cs, k=32))

def enc(s, encoding='ascii'):
    for _ in range(8):
        s = base64.b64encode(bytes(s, encoding)).decode(encoding)
    return s


offsets = []
offsets += [[18,0]]#0
offsets += [[32,0]]#1
offsets += [[37,20]]#2
offsets += [[48,27]]#3
offsets += [[64,36]]#4
offsets += [[70,48]]#5
offsets += [[77,63]]#6
offsets += [[93,64]]#7
offsets += [[98,84]]#8
offsets += [[107,86]]#9
offsets += [[122,95]]#10
offsets += [[127,112]]#11
offsets += [[137,115]]#12
offsets += [[153,128]]#13
offsets += [[157,139]]#14
offsets += [[168,150]]#15
offsets += [[183,154]]#16
offsets += [[187,171]]#17
offsets += [[195,179]]#18
offsets += [[210,186]]#19
offsets += [[218,200]]#20
offsets += [[225,206]]#21
offsets += [[243,218]]#22
offsets += [[250,228]]#23
offsets += [[257,239]]#24
offsets += [[273,248]]#25
offsets += [[277,264]]#26
offsets += [[288,267]]#27
offsets += [[300,275]]#28
offsets += [[307,291]]#29
offsets += [[342,299]]#30
offsets += [[342,304]]#31

f = open("db.txt","rb")
db = pickle.load(f)
f.close()



def revB64(data):
        strary = []
        for i in range(32):
                strary += [[]]
        for pos in range(1,31,1):
                offset = offsets[pos]
                tmp = data[offset[1]:offset[0]]
                strary[pos] = list(db[pos][tmp])
        for pos in range(1,30,1):
                now = []
                next = []
                for i in strary[pos]:
                        next2 = []
                        flag = False
                        for j in strary[pos+1]:
                                if i[1:3]==j[0:2]:
                                        next2 += [j]
                                        flag = True
                        if flag==True:
                                now += [i]
                                next.extend(next2)
                strary[pos] = now
                strary[pos+1] = list(set(next))
        for i in range(len(strary[1])):
                strary[0] += [strary[1][i][0]]
        for i in range(len(strary[30])):
                strary[31] += [strary[30][i][2]]
        for i in range(1,31,1):
                for j in range(len(strary[i])):
                        strary[i][j] = strary[i][j][1]
        for i in range(0,32,1):
                strary[i] = "".join(list(set(strary[i])))
        return strary


for i in range(100):
        chal = gen_chal()
        b64d = enc(chal)
        revB = revB64(b64d)
        Flag = "Good!"
        for i in revB:
                if len(i)!=1:
                        Flag = "Bad!"
                        break
        print("input: %s > %s %s" % (chal, Flag, ",".join(revB)))

結果は以下の通り。

input: othl6a2kk1733ybgrvy781o3uy3subnj > Good! o,t,h,l,6,a,2,k,k,1,7,3,3,y,b,g,r,v,y,7,8,1,o,3,u,y,3,s,u,b,n,j
input: vfitdek7870lji006cl8x8211fwaox2y > Good! v,f,i,t,d,e,k,7,8,7,0,l,j,i,0,0,6,c,l,8,x,8,2,1,1,f,w,a,o,x,2,y
input: yswbuwjg8o25ocvuluvbukoatm7bsw9p > Good! y,s,w,b,u,w,j,g,8,o,2,5,o,c,v,u,l,u,v,b,u,k,o,a,t,m,7,b,s,w,9,p
input: g80o1no172y7204x4c5i37cwo3fwrlwv > Good! g,8,0,o,1,n,o,1,7,2,y,7,2,0,4,x,4,c,5,i,3,7,c,w,o,3,f,w,r,l,w,v
input: dk02qwqwvb5qu31kp9ulspyjcxm0ex2q > Good! d,k,0,2,q,w,q,w,v,b,5,q,u,3,1,k,p,9,u,l,s,p,y,j,c,x,m,0,e,x,2,q
input: kwlmebccl0c25w98pklh88vk410dhp0j > Good! k,w,l,m,e,b,c,c,l,0,c,2,5,w,9,8,p,k,l,h,8,8,v,k,4,1,0,d,h,p,0,j
input: d50uttazyxw6auznmm0ar0faagawknmq > Good! d,5,0,u,t,t,a,z,y,x,w,6,a,u,z,n,m,m,0,a,r,0,f,a,a,g,a,w,k,n,m,q
input: eah0ypmesajh737arc5oikkbkg9rf9kc > Good! e,a,h,0,y,p,m,e,s,a,j,h,7,3,7,a,r,c,5,o,i,k,k,b,k,g,9,r,f,9,k,c
input: 7c9x8rn2i7gqldbwl071qgh5r5trsntx > Good! 7,c,9,x,8,r,n,2,i,7,g,q,l,d,b,w,l,0,7,1,q,g,h,5,r,5,t,r,s,n,t,x
input: mmjfwfrpt7kl4563wxr6e2xj0efno06i > Good! m,m,j,f,w,f,r,p,t,7,k,l,4,5,6,3,w,x,r,6,e,2,x,j,0,e,f,n,o,0,6,i
input: fcfp4xa2ih1v62df0g6t0xtbhp5d9al5 > Good! f,c,f,p,4,x,a,2,i,h,1,v,6,2,d,f,0,g,6,t,0,x,t,b,h,p,5,d,9,a,l,5
input: xp1pf4qvll9jpip7ytt1z6876x5jrp5k > Good! x,p,1,p,f,4,q,v,l,l,9,j,p,i,p,7,y,t,t,1,z,6,8,7,6,x,5,j,r,p,5,k
input: 8nj1uok390yuoiqyi1txr2siwdirf94e > Good! 8,n,j,1,u,o,k,3,9,0,y,u,o,i,q,y,i,1,t,x,r,2,s,i,w,d,i,r,f,9,4,e
input: 765qisnxyezxf2c6g71g30dcgg7b6z6i > Good! 7,6,5,q,i,s,n,x,y,e,z,x,f,2,c,6,g,7,1,g,3,0,d,c,g,g,7,b,6,z,6,i
input: t4i90gnbbknq1tv83zgjrma53kq7df9w > Good! t,4,i,9,0,g,n,b,b,k,n,q,1,t,v,8,3,z,g,j,r,m,a,5,3,k,q,7,d,f,9,w
input: h6z11jzo67mi4bqvlp82g1wnicxprfa0 > Good! h,6,z,1,1,j,z,o,6,7,m,i,4,b,q,v,l,p,8,2,g,1,w,n,i,c,x,p,r,f,a,0
input: ec91odcz04veshb7urejq6luvxp5v1ss > Good! e,c,9,1,o,d,c,z,0,4,v,e,s,h,b,7,u,r,e,j,q,6,l,u,v,x,p,5,v,1,s,s
input: btsvi06bvgkn0fbz2t5yxzubrxkcmu16 > Good! b,t,s,v,i,0,6,b,v,g,k,n,0,f,b,z,2,t,5,y,x,z,u,b,r,x,k,c,m,u,1,6
input: b9srequqfba9eyc4kf4ykba61eh6984p > Good! b,9,s,r,e,q,u,q,f,b,a,9,e,y,c,4,k,f,4,y,k,b,a,6,1,e,h,6,9,8,4,p
input: qazaqd2gg7999nnos1s8ph5jcv09360d > Good! q,a,z,a,q,d,2,g,g,7,9,9,9,n,n,o,s,1,s,8,p,h,5,j,c,v,0,9,3,6,0,d
input: 3eg66pmaidpy1sdz0bsg07mt2sx3jch4 > Good! 3,e,g,6,6,p,m,a,i,d,p,y,1,s,d,z,0,b,s,g,0,7,m,t,2,s,x,3,j,c,h,4
input: o4f7iykqtgtt08qii6k5tcn0lcorlj40 > Good! o,4,f,7,i,y,k,q,t,g,t,t,0,8,q,i,i,6,k,5,t,c,n,0,l,c,o,r,l,j,4,0
input: 8ori0gqgs3r79pk2e5l45j1f81fngd1p > Good! 8,o,r,i,0,g,q,g,s,3,r,7,9,p,k,2,e,5,l,4,5,j,1,f,8,1,f,n,g,d,1,p
input: 1bx8ewhlnyr9eqw0hex7h6yvhb89x2fd > Good! 1,b,x,8,e,w,h,l,n,y,r,9,e,q,w,0,h,e,x,7,h,6,y,v,h,b,8,9,x,2,f,d
input: twpj8ugv3yk1j2x2casr1x96bseuxxi4 > Good! t,w,p,j,8,u,g,v,3,y,k,1,j,2,x,2,c,a,s,r,1,x,9,6,b,s,e,u,x,x,i,4
input: gva5y1vnss1j4venfddel9al37iw9ib5 > Good! g,v,a,5,y,1,v,n,s,s,1,j,4,v,e,n,f,d,d,e,l,9,a,l,3,7,i,w,9,i,b,5
input: 09glhhkd0bk1scxbfux0yp60v50teur6 > Good! 0,9,g,l,h,h,k,d,0,b,k,1,s,c,x,b,f,u,x,0,y,p,6,0,v,5,0,t,e,u,r,6
input: v3ndqdgo82ptuz6hj8qj0p83xrg4ky4h > Good! v,3,n,d,q,d,g,o,8,2,p,t,u,z,6,h,j,8,q,j,0,p,8,3,x,r,g,4,k,y,4,h
input: 7bacwkfgc8t2eqynxg9fvt6b5oz680uc > Good! 7,b,a,c,w,k,f,g,c,8,t,2,e,q,y,n,x,g,9,f,v,t,6,b,5,o,z,6,8,0,u,c
input: tpmyexxgmw9u6chr1radj42idtk6m7x4 > Good! t,p,m,y,e,x,x,g,m,w,9,u,6,c,h,r,1,r,a,d,j,4,2,i,d,t,k,6,m,7,x,4
input: vrbps4ab6b601pg2vebohrjads7x69vn > Good! v,r,b,p,s,4,a,b,6,b,6,0,1,p,g,2,v,e,b,o,h,r,j,a,d,s,7,x,6,9,v,n
input: 47ap50ss7s1w6g8832k1bp7igls4an31 > Good! 4,7,a,p,5,0,s,s,7,s,1,w,6,g,8,8,3,2,k,1,b,p,7,i,g,l,s,4,a,n,3,1
input: 4c2b88s4tet8ufsfr33un3cpuxmze92n > Good! 4,c,2,b,8,8,s,4,t,e,t,8,u,f,s,f,r,3,3,u,n,3,c,p,u,x,m,z,e,9,2,n
input: e9mfuxzlksiegeng8rfinxhn5dw37av1 > Good! e,9,m,f,u,x,z,l,k,s,i,e,g,e,n,g,8,r,f,i,n,x,h,n,5,d,w,3,7,a,v,1
input: wys9ucd2lfg37rlpr0vjzxagfjd2aln0 > Good! w,y,s,9,u,c,d,2,l,f,g,3,7,r,l,p,r,0,v,j,z,x,a,g,f,j,d,2,a,l,n,0
input: u87ele00q364ulo2fvhnnplf5uno1m5k > Good! u,8,7,e,l,e,0,0,q,3,6,4,u,l,o,2,f,v,h,n,n,p,l,f,5,u,n,o,1,m,5,k
input: 1wa7zcqn5k9m26ojaoi31w14umfv28h9 > Good! 1,w,a,7,z,c,q,n,5,k,9,m,2,6,o,j,a,o,i,3,1,w,1,4,u,m,f,v,2,8,h,9
input: iupe1tvnpaln779vkwlxhweugnf5ofc0 > Good! i,u,p,e,1,t,v,n,p,a,l,n,7,7,9,v,k,w,l,x,h,w,e,u,g,n,f,5,o,f,c,0
input: u9g6yh2jzj11erguhsnv45e0mp458ajl > Good! u,9,g,6,y,h,2,j,z,j,1,1,e,r,g,u,h,s,n,v,4,5,e,0,m,p,4,5,8,a,j,l
input: z4kia5vxrov2l0ukm5o70ba1gmz6zwhv > Good! z,4,k,i,a,5,v,x,r,o,v,2,l,0,u,k,m,5,o,7,0,b,a,1,g,m,z,6,z,w,h,v
input: xtxxpyw37wohzi4eq239by0j7sooq6n7 > Good! x,t,x,x,p,y,w,3,7,w,o,h,z,i,4,e,q,2,3,9,b,y,0,j,7,s,o,o,q,6,n,7
input: lfc8kdf0hxt0u03r58ixquba9pi72t83 > Good! l,f,c,8,k,d,f,0,h,x,t,0,u,0,3,r,5,8,i,x,q,u,b,a,9,p,i,7,2,t,8,3
input: 1g547miny0tev5en6fxc8l3brb71xdh7 > Good! 1,g,5,4,7,m,i,n,y,0,t,e,v,5,e,n,6,f,x,c,8,l,3,b,r,b,7,1,x,d,h,7
input: mqkue86yji59updt9boqcjmfjnt2z7fp > Good! m,q,k,u,e,8,6,y,j,i,5,9,u,p,d,t,9,b,o,q,c,j,m,f,j,n,t,2,z,7,f,p
input: nzxqoqq1tr2r1niwwib2xj7fjjvzoq3s > Good! n,z,x,q,o,q,q,1,t,r,2,r,1,n,i,w,w,i,b,2,x,j,7,f,j,j,v,z,o,q,3,s
input: 0zqt8nw21ifmwnxsjcdocfkhp0go3bbv > Good! 0,z,q,t,8,n,w,2,1,i,f,m,w,n,x,s,j,c,d,o,c,f,k,h,p,0,g,o,3,b,b,v
input: vp8u1bouepqs4qz6jqmg0qjcbha67nfy > Good! v,p,8,u,1,b,o,u,e,p,q,s,4,q,z,6,j,q,m,g,0,q,j,c,b,h,a,6,7,n,f,y
input: 59ojs64iafa1ziojfd5zl0nxgnongpgc > Good! 5,9,o,j,s,6,4,i,a,f,a,1,z,i,o,j,f,d,5,z,l,0,n,x,g,n,o,n,g,p,g,c
input: ffge48vdzgheoz2326o5bi7vlutre6bs > Good! f,f,g,e,4,8,v,d,z,g,h,e,o,z,2,3,2,6,o,5,b,i,7,v,l,u,t,r,e,6,b,s
input: aebsdw442i60fexoq8py908jmz2mby0b > Good! a,e,b,s,d,w,4,4,2,i,6,0,f,e,x,o,q,8,p,y,9,0,8,j,m,z,2,m,b,y,0,b
input: yxyb4mi3zn0yl09pleaj8mzalv06wjjx > Good! y,x,y,b,4,m,i,3,z,n,0,y,l,0,9,p,l,e,a,j,8,m,z,a,l,v,0,6,w,j,j,x
input: cwwpgwsava8s4awii0zfh2fy3xm51q8y > Good! c,w,w,p,g,w,s,a,v,a,8,s,4,a,w,i,i,0,z,f,h,2,f,y,3,x,m,5,1,q,8,y
input: k9hsmu9vljnlcwk5yvh29r1i3zekxetq > Good! k,9,h,s,m,u,9,v,l,j,n,l,c,w,k,5,y,v,h,2,9,r,1,i,3,z,e,k,x,e,t,q
input: uahks5p8snxh7vxy6r3q6xt97wrqzage > Good! u,a,h,k,s,5,p,8,s,n,x,h,7,v,x,y,6,r,3,q,6,x,t,9,7,w,r,q,z,a,g,e
input: adg0p4wjh5jihk9qkt5idwbeei2scwv5 > Good! a,d,g,0,p,4,w,j,h,5,j,i,h,k,9,q,k,t,5,i,d,w,b,e,e,i,2,s,c,w,v,5
input: t6onpsup8s23xlbrnbeu2n7wtaf3pdiq > Good! t,6,o,n,p,s,u,p,8,s,2,3,x,l,b,r,n,b,e,u,2,n,7,w,t,a,f,3,p,d,i,q
input: 20ptjjzbpr9ir032yhqm35ahj94ai6d2 > Good! 2,0,p,t,j,j,z,b,p,r,9,i,r,0,3,2,y,h,q,m,3,5,a,h,j,9,4,a,i,6,d,2
input: f731l4utp5bo1jm53m8vp9x7bmddxu55 > Good! f,7,3,1,l,4,u,t,p,5,b,o,1,j,m,5,3,m,8,v,p,9,x,7,b,m,d,d,x,u,5,5
input: v8elnpjqkw3b9j5xg4kcyk718kh1114f > Good! v,8,e,l,n,p,j,q,k,w,3,b,9,j,5,x,g,4,k,c,y,k,7,1,8,k,h,1,1,1,4,f
input: jjmon470rlj4ngsvrdi7ahne8rhh8w8s > Good! j,j,m,o,n,4,7,0,r,l,j,4,n,g,s,v,r,d,i,7,a,h,n,e,8,r,h,h,8,w,8,s
input: rq93s675pd6c62ksmzlpp1jq88gqnklh > Good! r,q,9,3,s,6,7,5,p,d,6,c,6,2,k,s,m,z,l,p,p,1,j,q,8,8,g,q,n,k,l,h
input: 87isgaowjnoe5oujiiatzpk9a2d2y6v6 > Good! 8,7,i,s,g,a,o,w,j,n,o,e,5,o,u,j,i,i,a,t,z,p,k,9,a,2,d,2,y,6,v,6
input: yeqocmlii5roahyqr97fil14fqj1hx4d > Good! y,e,q,o,c,m,l,i,i,5,r,o,a,h,y,q,r,9,7,f,i,l,1,4,f,q,j,1,h,x,4,d
input: ue636diqh8tf2lagt1xn6ts6jjtp38hc > Good! u,e,6,3,6,d,i,q,h,8,t,f,2,l,a,g,t,1,x,n,6,t,s,6,j,j,t,p,3,8,h,c
input: de4wi1b2phq5zrsdhlnbqnx5x824xjax > Good! d,e,4,w,i,1,b,2,p,h,q,5,z,r,s,d,h,l,n,b,q,n,x,5,x,8,2,4,x,j,a,x
input: oyhj3eezt46yt5gza6izbjfr78q898ub > Good! o,y,h,j,3,e,e,z,t,4,6,y,t,5,g,z,a,6,i,z,b,j,f,r,7,8,q,8,9,8,u,b
input: 2bz8eo8mznhx0zzv7kueybjmpm20kfkn > Good! 2,b,z,8,e,o,8,m,z,n,h,x,0,z,z,v,7,k,u,e,y,b,j,m,p,m,2,0,k,f,k,n
input: 95h40q8ayos39j88sjzg7ujk36qlc4qd > Good! 9,5,h,4,0,q,8,a,y,o,s,3,9,j,8,8,s,j,z,g,7,u,j,k,3,6,q,l,c,4,q,d
input: ixezg8b87snprzrl5g542rv9mox9unnh > Good! i,x,e,z,g,8,b,8,7,s,n,p,r,z,r,l,5,g,5,4,2,r,v,9,m,o,x,9,u,n,n,h
input: b3e2mxv3yjaxsvbfhzs4zm1v6cq0w93u > Good! b,3,e,2,m,x,v,3,y,j,a,x,s,v,b,f,h,z,s,4,z,m,1,v,6,c,q,0,w,9,3,u
input: al2z8du84somda36m3w31zj0mi0d7spy > Good! a,l,2,z,8,d,u,8,4,s,o,m,d,a,3,6,m,3,w,3,1,z,j,0,m,i,0,d,7,s,p,y
input: lli0f9kqsdazjsaz3o92lgdrkgmu3q1n > Good! l,l,i,0,f,9,k,q,s,d,a,z,j,s,a,z,3,o,9,2,l,g,d,r,k,g,m,u,3,q,1,n
input: x32fghny749ce8zhq9bpm356qp5zezxm > Good! x,3,2,f,g,h,n,y,7,4,9,c,e,8,z,h,q,9,b,p,m,3,5,6,q,p,5,z,e,z,x,m
input: t1008vz85afxq10863wpv9d53hgjlczu > Good! t,1,0,0,8,v,z,8,5,a,f,x,q,1,0,8,6,3,w,p,v,9,d,5,3,h,g,j,l,c,z,u
input: nu15p5h2n823h8bf14y2bpd4t0k0bbjq > Good! n,u,1,5,p,5,h,2,n,8,2,3,h,8,b,f,1,4,y,2,b,p,d,4,t,0,k,0,b,b,j,q
input: tjecw7prsa3tpb0tit4ks5lu6nz3hyvm > Good! t,j,e,c,w,7,p,r,s,a,3,t,p,b,0,t,i,t,4,k,s,5,l,u,6,n,z,3,h,y,v,m
input: ztv8orml0td7s4rfcbdhqlti5y229dq2 > Good! z,t,v,8,o,r,m,l,0,t,d,7,s,4,r,f,c,b,d,h,q,l,t,i,5,y,2,2,9,d,q,2
input: a6usu8y87rhu2yltle4k445izlkeztw4 > Good! a,6,u,s,u,8,y,8,7,r,h,u,2,y,l,t,l,e,4,k,4,4,5,i,z,l,k,e,z,t,w,4
input: fmvyjht6cm7qon57f2umxzx4namytgpg > Good! f,m,v,y,j,h,t,6,c,m,7,q,o,n,5,7,f,2,u,m,x,z,x,4,n,a,m,y,t,g,p,g
input: sb160x7dim4yixizz8u142n2x3tcrqt5 > Good! s,b,1,6,0,x,7,d,i,m,4,y,i,x,i,z,z,8,u,1,4,2,n,2,x,3,t,c,r,q,t,5
input: ms9dqof5v6q25ntzlcyt9eltwi13ujpd > Good! m,s,9,d,q,o,f,5,v,6,q,2,5,n,t,z,l,c,y,t,9,e,l,t,w,i,1,3,u,j,p,d
input: cwj776o3r9goqr0gked76eu3soqowlj2 > Good! c,w,j,7,7,6,o,3,r,9,g,o,q,r,0,g,k,e,d,7,6,e,u,3,s,o,q,o,w,l,j,2
input: fe4r5u541fo721lzxyap11lno7r1fncv > Good! f,e,4,r,5,u,5,4,1,f,o,7,2,1,l,z,x,y,a,p,1,1,l,n,o,7,r,1,f,n,c,v
input: xrc1wuzp6g1g7irp8a8dt9m5jvwg5cad > Good! x,r,c,1,w,u,z,p,6,g,1,g,7,i,r,p,8,a,8,d,t,9,m,5,j,v,w,g,5,c,a,d
input: avy6a9jh72zfhfdvxcm18ah7fx2e9u0w > Good! a,v,y,6,a,9,j,h,7,2,z,f,h,f,d,v,x,c,m,1,8,a,h,7,f,x,2,e,9,u,0,w
input: 0g7sb8ri60x6m3q4dgkfiebqt5yrznhd > Good! 0,g,7,s,b,8,r,i,6,0,x,6,m,3,q,4,d,g,k,f,i,e,b,q,t,5,y,r,z,n,h,d
input: uxgc1tqzj6u8dc07jwkqgs5by8m5lohl > Good! u,x,g,c,1,t,q,z,j,6,u,8,d,c,0,7,j,w,k,q,g,s,5,b,y,8,m,5,l,o,h,l
input: vke3gqekxtase0hnuc7e6r0axzkkp0bj > Good! v,k,e,3,g,q,e,k,x,t,a,s,e,0,h,n,u,c,7,e,6,r,0,a,x,z,k,k,p,0,b,j
input: xfns86lv79nsk96bxud52uufblmazp27 > Good! x,f,n,s,8,6,l,v,7,9,n,s,k,9,6,b,x,u,d,5,2,u,u,f,b,l,m,a,z,p,2,7
input: 28xgrfcwzncjo5lxunx8zm09d25hr6mb > Good! 2,8,x,g,r,f,c,w,z,n,c,j,o,5,l,x,u,n,x,8,z,m,0,9,d,2,5,h,r,6,m,b
input: kwnla4jpm7f0n4vy9pv3cbxupy3x9gfj > Good! k,w,n,l,a,4,j,p,m,7,f,0,n,4,v,y,9,p,v,3,c,b,x,u,p,y,3,x,9,g,f,j
input: vw4j7t41xfx2v4qp5woqhf3grng3yn6b > Good! v,w,4,j,7,t,4,1,x,f,x,2,v,4,q,p,5,w,o,q,h,f,3,g,r,n,g,3,y,n,6,b
input: hfr806ulsyvd85uk2otddiddbx30i7hm > Good! h,f,r,8,0,6,u,l,s,y,v,d,8,5,u,k,2,o,t,d,d,i,d,d,b,x,3,0,i,7,h,m
input: ozwmnbz1umau700urjnlhj4zgvx0sz9a > Good! o,z,w,m,n,b,z,1,u,m,a,u,7,0,0,u,r,j,n,l,h,j,4,z,g,v,x,0,s,z,9,a
input: kohemfmo0d64roxwapr8zu0e715lz9o0 > Good! k,o,h,e,m,f,m,o,0,d,6,4,r,o,x,w,a,p,r,8,z,u,0,e,7,1,5,l,z,9,o,0
input: xofu6sus13xa20dxjfgrli9fd9xvdxcs > Good! x,o,f,u,6,s,u,s,1,3,x,a,2,0,d,x,j,f,g,r,l,i,9,f,d,9,x,v,d,x,c,s
input: wgjnetmyyswvj0gsv765urh1ufsvetae > Good! w,g,j,n,e,t,m,y,y,s,w,v,j,0,g,s,v,7,6,5,u,r,h,1,u,f,s,v,e,t,a,e
input: 9f9t1erull3x3kdud8rr4np9s9i2tqee > Good! 9,f,9,t,1,e,r,u,l,l,3,x,3,k,d,u,d,8,r,r,4,n,p,9,s,9,i,2,t,q,e,e
input: j0lqom9er039ruzmp5uokr1xgz54vqwq > Good! j,0,l,q,o,m,9,e,r,0,3,9,r,u,z,m,p,5,u,o,k,r,1,x,g,z,5,4,v,q,w,q
input: p1epwrdi0rskhm39mja9psrvcrrb0ijy > Good! p,1,e,p,w,r,d,i,0,r,s,k,h,m,3,9,m,j,a,9,p,s,r,v,c,r,r,b,0,i,j,y

完璧に復元できることがわかった。そしてmaskされた場合に復元を行うsolverは以下になる。辞書配列のデータベースのkeyをmaskされたものに置き換えるようになっている。

solverimport random
import string
import base64
import os
import pickle

cs = string.ascii_lowercase + string.digits



def gen_chal():
    cs = string.ascii_lowercase + string.digits
    return ''.join(random.choices(cs, k=32))

def gen_chal5(forward,char,back,pos):
    if pos==0:
        return char + back + ''.join(random.choices(cs, k=30-pos))
    elif pos==31:
        return ''.join(random.choices(cs, k=pos-1)) + forward + char
    else : 
        return ''.join(random.choices(cs, k=pos-1)) + forward + char + back + ''.join(random.choices(cs, k=30-pos))


def enc(s, encoding='ascii'):
    for _ in range(8):
        s = base64.b64encode(bytes(s, encoding)).decode(encoding)
    return s


offsets = []
offsets += [[18,0]]#0
offsets += [[32,0]]#1
offsets += [[37,20]]#2
offsets += [[48,27]]#3
offsets += [[64,36]]#4
offsets += [[70,48]]#5
offsets += [[77,63]]#6
offsets += [[93,64]]#7
offsets += [[98,84]]#8
offsets += [[107,86]]#9
offsets += [[122,95]]#10
offsets += [[127,112]]#11
offsets += [[137,115]]#12
offsets += [[153,128]]#13
offsets += [[157,139]]#14
offsets += [[168,150]]#15
offsets += [[183,154]]#16
offsets += [[187,171]]#17
offsets += [[195,179]]#18
offsets += [[210,186]]#19
offsets += [[218,200]]#20
offsets += [[225,206]]#21
offsets += [[243,218]]#22
offsets += [[250,228]]#23
offsets += [[257,239]]#24
offsets += [[273,248]]#25
offsets += [[277,264]]#26
offsets += [[288,267]]#27
offsets += [[300,275]]#28
offsets += [[307,291]]#29
offsets += [[342,299]]#30
offsets += [[342,304]]#31

db = []
for i in range(32):
        db += [{}]

f = open("db.txt","rb")
db = pickle.load(f)
f.close()

def revB64(data):
        strary = []
        for i in range(32):
                strary += [[]]
        for pos in range(1,31,1):
                offset = offsets[pos]
                tmp = data[offset[1]:offset[0]]
                strary[pos] = list(db[pos][tmp])
        for pos in range(1,30,1):
                now = []
                next = []
                for i in strary[pos]:
                        next2 = []
                        flag = False
                        for j in strary[pos+1]:
                                if i[1:3]==j[0:2]:
                                        next2 += [j]
                                        flag = True
                        if flag==True:
                                now += [i]
                                next.extend(next2)
                strary[pos] = now
                strary[pos+1] = list(set(next))
        for i in range(len(strary[1])):
                strary[0] += [strary[1][i][0]]
        for i in range(len(strary[30])):
                strary[31] += [strary[30][i][2]]
        for i in range(1,31,1):
                for j in range(len(strary[i])):
                        strary[i][j] = strary[i][j][1]
        for i in range(0,32,1):
                strary[i] = "".join(list(set(strary[i])))
        return strary



while True:
        _mask = input('mask: ')
        if len(_mask) != 344 or any(c not in '*?' for c in _mask):
                print('bad mask :(')
                continue
        fullmask = list(_mask)
        break

submask = []
db2 = []
for i in range(32):
        db2 += [{}]
for pos in range(1,31,1):
        mask = fullmask[offsets[pos][1]:offsets[pos][0]]
        submask += ["".join(mask)]
        for i in db[pos]:
                masked = ''.join('*' if m == '*' else c for m, c in zip(mask, i))
                if masked not in db2[pos]:
                        db2[pos][masked] = []
                db2[pos][masked].extend(db[pos][i])

db = db2
while True:
        data = input('base64: ')
        revB = revB64(data)
        Flag = "Good!"
        for i in revB:
                if len(i)!=1:
                        Flag = "Bad!"
                        break
        if Flag=="Bad!":
                print("%s %s" % (Flag, ",".join(revB)))
        else :
                print("%s %s" % (Flag, "".join(revB)))

しかしCTF中、文字のバリエーションが最も多い桁を探したり、文字の変化が最も激しい桁を探すといった方法で様々なmaskを作ってみたものの完璧に復元できるmaskを得ることは出来なかった。

例えば次のようなmaskを入力すると、各桁の候補を一意に絞り込めず失敗する。

mask: ********?**?*?*?**?*?***??***?**????*?*???***??**??*****?***???***?********?*?*?*????*****?********?*?*******???*?*****??****???*????*****?**?***????*??*****???*?*?***????**???***??***?****?****?????**??*******???*??*??**?*?**?***????*****?********?***??*****???????***?**********??*?*???**??*****??**???*****???*******?*?**********************
base64: ********T**V*G*W**d*T***MW***m**WkZO*l*sWj***WR**jF*****Q***Vll***Z********X*F*X*Wxae*****R********W*V*******Epv*V*****HR****EpJ*ld0V*****d**m***TFaM*Zs*****mRz*2*4***EVll**kkx***kS***Z****0****xoU1R**Xh*******twe*Yy*zF**k*J**x***ZFa3*****H********S***bH*****4b1UwMH***G**********aE*T*GRy**t0*****jR**VJH*****FVY*******Y*W**********************
Bad! f,n,fbr,ea,12,mi,k,r,cmbn,4d,54,q,c,p,8,g,n,hs,7,51,j,5,1,abqcsr,p,t,hg,j2zv,0,d,t,o

CTF終了後、作問者のWriteup( https://hackmd.io/@mikit/H1k7A68EK )で提示されているmaskを入力したところ、以下のように完全に復元された。

mask: *******??**?***?***???***?*??****??**??**?**?*?**???***?***?*??***?**?**?****?*?**???**?**?**?*?***???*?*****?*???***??***?**?*?*?**?**?**?**?*?***?**??***?*?*?***?***?*?***?*??**??***?****?*?****?*?????****??**?*****??****?**?****???*??**??****??*?***??*?****?*?*??***??*****?*?**?****?**???***?**?**??******???**??***?*?**********************
base64: *******Gb**S***X***SWV***D*WM****25**lJ**H**a*M**0Za***G***S*ll***c**1**T****0*X**xwb**X**F**1*H***Wal*t*****3*hU1***1d***R**l*I*T**R**H**h**G*a***S**pW***W*V*0***S***U*k***z*wV**aW***b****z*h****Q*dGcFd****XT**a*****mF****2**x****Fb3*Wa**rU****Vd*b***V2*X****e*U*NX***lp*****W*V**k****R**2xO***V**R**FZ******mFI**FS***h*l**********************
Good! 2en4ftwd6e6ewt0h8pswdv9z5f9n2aku

この問題はmaskの作り方が極めて重要なポイントであったようだ。次の画像はこのwriteupのようなコードに整理する前のものだが、作問者のmaskで問題が解けた様子。

問題解答時の様子