TUCTF 2018 Writeup

この大会は2018/11/24 8:00(JST)~2018/11/26 8:00(JST)に開催されました。
今回もチームで参戦。結果は 7219点で764チーム中65位でした。
自分で解けた問題をWriteupとして書いておきます。

Welcome (Welcome)

Discordの#welcomeチャネルにフラグが書いてあった。

TUCTF{my_f1r57_fl46}

Mrs. White's <br> Messy Maids (Web)

ソースのコメントに以下が書いてある。

<!-- I might kill if I could find him. Stupid Mr. /Boddy -->

http://18.218.152.56/Boddy/にアクセスすると、フラグが書いてあった。

TUCTF{1_4ccu53_Mr5._Wh173_w17h_7h3_c4ndl3571ck_1n_7h3_c0mm3n75}

Literal (Misc)

$ curl http://18.222.124.7
<html>

<head>
	<meta http-equiv="refresh" content="0; URL='Literal.html'" />
</head>

</html>
$ curl http://18.222.124.7/Literal.html
<html>
  <head>
  <meta http-equiv="Refresh" content="1; url=https://en.wikipedia.org/wiki/Fork_bomb">
  </head>
  <body>
  <!--
        *   *                     f   f   f
      *  ** *                   ff  ff  ff
      * * ** ||                ff  ff  ff
    **   ||||T||              fUffffffff
      *   |C|||T| oooooooooooo fFff
           |||||||{ooooooooooRfff3o
          ooo4ooooooooooooooLff.ooooo0
        oooNooooooooooooooo3ooooooo5ooo.
        oooo4oooooRoooooooooo3oooooooooo.
        oooDooooo4oooooNooooooooooooooooo
        ooooooooooooooGoooooooooooooooooo
        ooooooooooooooooooooo3oooooooooRo
         oooooooo0oooooooooooooooooooooo
          oooooooffUoooooooooooooooooo
            ooofff5ooooooooooooooooo
             fff }ooooooooooooooo
            fff
  -->
  Redirecting to Wikipedia...!
  </body>
</html>

英大文字、数字、'{', '}', '.'を抜き出す。

import string
comment = '''
        *   *                     f   f   f
      *  ** *                   ff  ff  ff
      * * ** ||                ff  ff  ff
    **   ||||T||              fUffffffff
      *   |C|||T| oooooooooooo fFff
           |||||||{ooooooooooRfff3o
          ooo4ooooooooooooooLff.ooooo0
        oooNooooooooooooooo3ooooooo5ooo.
        oooo4oooooRoooooooooo3oooooooooo.
        oooDooooo4oooooNooooooooooooooooo
        ooooooooooooooGoooooooooooooooooo
        ooooooooooooooooooooo3oooooooooRo
         oooooooo0oooooooooooooooooooooo
          oooooooffUoooooooooooooooooo
            ooofff5ooooooooooooooooo
             fff }ooooooooooooooo
            fff
'''

chars = string.uppercase + string.digits + '{}.'

flag = ''
for c in comment:
    if c in chars:
        flag += c

print flag
TUCTF{R34L.0N35.4R3.D4NG3R0U5}

RSAyyyy (Crypto)

RSAの基本的な計算をして答えていく。

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

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(('3.16.57.250', 12345))

## Level 1 ##
data = recvuntil(s, 'n?\n').strip()
print data
pattern = 'p = (.+)\nq = (.+)\n'
m = re.search(pattern, data)
p = int(m.group(1))
q = int(m.group(2))
n = p * q
print n
s.sendall(str(n) + '\n')

## Level 2 ##
data = recvuntil(s, 'm?\n').strip()
print data
pattern = 'message = \"(.+)\"'
m = re.search(pattern, data)
message = m.group(1)
m = bytes_to_long(message)
print m
s.sendall(str(m) + '\n')

## Level 3 ##
data = recvuntil(s, 'n?\n').strip()
print data
pattern = 'p = (.+)\nq = (.+)\n'
m = re.search(pattern, data)
p = int(m.group(1))
q = int(m.group(2))
n = p * q
print n
s.sendall(str(n) + '\n')

data = recvuntil(s, 'c?\n').strip()
print data
pattern = 'e = (.+)\nm = (.+)\n'
m = re.search(pattern, data)
e = int(m.group(1))
m = int(m.group(2))
c = pow(m, e, p * q)
print c
s.sendall(str(c) + '\n')

## Level 4 ##
data = recvuntil(s, '(n)?\n').strip()
print data
pattern = 'p = (.+)\nq = (.+)\n'
m = re.search(pattern, data)
p = int(m.group(1))
q = int(m.group(2))
phi = (p - 1) * (q - 1)
print phi
s.sendall(str(phi) + '\n')

data = recvuntil(s, 'd?\n').strip()
print data
pattern = 'e = (.+)\n'
m = re.search(pattern, data)
e = int(m.group(1))
d = inverse(e, phi)
print d
s.sendall(str(d) + '\n')

## Level 5 ##
data = recvuntil(s, 'p?\n').strip()
print data
pattern = 'n = (.+)\n'
m = re.search(pattern, data)
n = int(m.group(1))
fac = sympy.factorint(n)
p = fac.keys()[0]
print p
s.sendall(str(p) + '\n')

data = recvuntil(s, 'q?\n').strip()
print data
q = n / p
print q
s.sendall(str(q) + '\n')

## Level 6 ##
data = recvuntil(s, 'p?\n').strip()
print data
pattern = 'c = (.+)\nn = (.+)\ne = (.+)\n'
m = re.search(pattern, data)
c = int(m.group(1))
n = int(m.group(2))
e = int(m.group(3))
fac = sympy.factorint(n)
p = fac.keys()[0]
print p
s.sendall(str(p) + '\n')

data = recvuntil(s, 'q?\n').strip()
print data
q = n / p
print q
s.sendall(str(q) + '\n')

data = recvuntil(s, '(n)?\n').strip()
print data
phi = (p - 1) * (q - 1)
print phi
s.sendall(str(phi) + '\n')

data = recvuntil(s, 'd?\n').strip()
print data
d = inverse(e, phi)
print d
s.sendall(str(d) + '\n')

data = recvuntil(s, 'm?\n').strip()
print data
m = pow(c, d, n)
print m
s.sendall(str(m) + '\n')

data = s.recv(2048)
print data

実行結果は以下のとおり。

$ python solve.py
               AAA               YYYYYYY       YYYYYYYYYYYYYY       YYYYYYYYYYYYYY       YYYYYYY
              A:::A              Y:::::Y       Y:::::YY:::::Y       Y:::::YY:::::Y       Y:::::Y
             A:::::A             Y:::::Y       Y:::::YY:::::Y       Y:::::YY:::::Y       Y:::::Y
            A:::::::A            Y::::::Y     Y::::::YY::::::Y     Y::::::YY::::::Y     Y::::::Y
           A:::::::::A           YYY:::::Y   Y:::::YYYYYY:::::Y   Y:::::YYYYYY:::::Y   Y:::::YYY
          A:::::A:::::A             Y:::::Y Y:::::Y      Y:::::Y Y:::::Y      Y:::::Y Y:::::Y
         A:::::A A:::::A             Y:::::Y:::::Y        Y:::::Y:::::Y        Y:::::Y:::::Y
        A:::::A   A:::::A             Y:::::::::Y          Y:::::::::Y          Y:::::::::Y
       A:::::A     A:::::A             Y:::::::Y            Y:::::::Y            Y:::::::Y
      A:::::AAAAAAAAA:::::A             Y:::::Y              Y:::::Y              Y:::::Y
     A:::::::::::::::::::::A            Y:::::Y              Y:::::Y              Y:::::Y
    A:::::AAAAAAAAAAAAA:::::A           Y:::::Y              Y:::::Y              Y:::::Y
   A:::::A             A:::::A          Y:::::Y              Y:::::Y              Y:::::Y
  A:::::A               A:::::A      YYYY:::::YYYY        YYYY:::::YYYY        YYYY:::::YYYY
 A:::::A                 A:::::A     Y:::::::::::Y        Y:::::::::::Y        Y:::::::::::Y
AAAAAAA                   AAAAAAA    YYYYYYYYYYYYY        YYYYYYYYYYYYY        YYYYYYYYYYYYY

This challenge is designed to act as an introduction to RSA.
If you have a team member that is not already familiar with RSA,
then give this challenge to them.

For the first level, I recommend looking at
https://simple.wikipedia.org/wiki/RSA_algorithm
but any description of the RSA algorithm will do.

Later levels will probably require further research.

Let's get started!



Level 1: Calculating n

p = 4069127677
q = 2789804453
What is n?
11352070513120145681
Whoop whoop!

Congratulations! You beat Level 1!

In order to calculate the ciphertext,
the message needs to be converted to an integer.


Level 2: Calculating m

message = "epidemiology ruminant posy provident condemnatory"
What is m?
3996904368156142538059084847112340597446843790219386982825830622566068551003043414749611727228669241263353749935452793
Yeah! You do that RSA!

Congratulations! You beat Level 2!

Now, we are going to actually calculate ciphertext.


Level 3: Calculating c

p = 2443367911
q = 2356648727
What is n?
5758159877050799297
Whoop whoop!

e = 65537
m = 113667960107631
What is c?
1146535438296311460
Nice job!

Congratulations! You beat Level 3!

In order for RSA to be asymmetrical,
the private exponent, d, needs to be calculated.


Level 4: Calculating d

p = 3288422173
q = 4282201841
What is tot(n)?
14081687475635196480
Ayyyyy

e = 65537
What is d?
4110390793122988673
Way to go!

Congratulations! You beat Level 4!

The easiest way to break RSA is factoring n, if it is possible.


Level 5: Factoring n

n = 6158577329169411137
What is p?
2316690071
Nice job!

What is q?
2658351847
Yeah! You do that RSA!

Congratulations! You beat Level 5!

Now, let's put everything together and break RSA!


Level 6: Breaking simple RSA

c = 5755984274299477791
n = 13316707131108976583
e = 65537
What is p?
3710062907
Way to go!

What is q?
3589348069
Yeah! You do that RSA!

What is tot(n)?
13316707123809565608
Way to go!

What is d?
4877462163356955809
Nice job!

Finally, what is m?
418548049262

That was adequate.

Congratulations! You beat Level 6!


Congratulations on finishing this introduction to RSA!
I hope this was fun and informative.

Here's your flag:
TUCTF{RSA_1$_R34LLY_C00L_4ND_1MP0RT4NT_CRYPT0}
TUCTF{RSA_1$_R34LLY_C00L_4ND_1MP0RT4NT_CRYPT0}

AESential Lesson (Crypto)

$ nc 18.218.238.95 12345

Lol. You think you can steal my flag?
I'll even encrypt your input for you,
but you can't get my secrets!

Enter your text here: 11
Here's your encrypted text:
61537177fe7ceb9cc7eb9e57c64fc9a5492544386adac7645752ed14e253dc501c2da954187caa36ed893ca14c05e767ae1c6abac2df0bbffea785521c616e54

フラグを1文字ずつはみ出させ、ブルートフォースでブロック単位で暗号が一致するものを求めていく。

0123456789abcdef
1111111111111111
111111111111111A
1111111111111111
111111111111111F
FFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFP

※F: フラグ(32文字)
import socket

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(('18.218.238.95', 12345))

flag = ''
for i in range(32):
    for code in range(32, 127):
        plain = '#' * (31 - i) + flag + chr(code) + '#' * (31 - i)
        data = recvuntil(s, 'here: ')
        print data + plain
        s.sendall(plain + '\n')
        data = recvuntil(s, '\n')
        cipher = recvuntil(s, '\n').strip()
        print data + cipher
        if cipher[32:64] == cipher[96:128]:
            flag += chr(code)
            break

print flag
TUCTF{A3S_3CB_1S_VULN3R4BL3!!!!}

Jimmy's Crypto (Crypto)

2つのファイルが同じXOR鍵で暗号化されている。https://github.com/SpiderLabs/cribdragのツールを使って、英単語を推測しながら復号していく。
まず、2つの暗号ファイルのXORを16進数で取得する。

def str_xor(s1, s2):
    return ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(s1, s2))

with open('secret', 'rb') as f:
    enc_secret = f.read()

with open('flag', 'rb') as f:
    enc_flag = f.read()

xor_str = str_xor(enc_secret, enc_flag)
print xor_str.encode('hex')

'secret' や'this'などよく出てきそうな英単語から推測していく。フラグより後は復号していないが、最終的には以下の結果。

>cribdrag.py 2a1106060001125604001100061c45155512010317475a310b50461c1743060d1d5d00370a0d5511540d2030223866163d103d313c2736472c7a687a39734d22467f2a413c7f2a5f3d343d26547f32406d4737244073355d2b56515e0f00010c5200031415411a06184f532765
                                               :
Your message is currently:
0       This is a secret file. Top secret. Don't
40       steal my secrets. If you are looking at
80       this file,__________________
Your key is currently:
0       ~you have been authorized for this secre
40      t~TUCTF{D0NT_US3_TH3_S4M3_K3Y_F0R_TH3_S4
80      M3_M3SS4G3}__________________
TUCTF{D0NT_US3_TH3_S4M3_K3Y_F0R_TH3_S4M3_M3SS4G3}

RITSEC CTF 2018 Writeup

この大会は2018/11/17 2:00(JST)~2018/11/19 14:00(JST)に開催されました。
今回もチームで参戦。結果は 4236点で952チーム中18位でした。
自分で解けた問題をWriteupとして書いておきます。

Litness Test (Misc 1)

問題にフラグが書いてある。

RITSEC{welc0me_t0_th3_CTF!}

Talk to me (Misc 10)

Discordの#announcementsチャネルでフラグを見つけた。

RITSEC{its_like_irc-but_with_2_much_javascript}

Check out this cool filter (Misc 200)

Youtubeのタイトルは「Eiffel 65 - Blue (Da Ba Dee)」。Blueが関係あるのかもしれない。
Blueの値を取得してみると、同じ数値の並びが繰り返されている。
繰り返しを外し、シフトするとフラグになりそうだったので試してみる。

from PIL import Image

img = Image.open('CheckOutThisFilter.png').convert('RGB')
w, h = img.size

codes = []
for y in range(0, h):
    for x in range(0, w):
        r, g, b = img.getpixel((x, y))
        codes.append(b)

codes = codes[:51]

flag = ''
for code in codes:
    flag += chr(code - 13)

print flag
RITSEC{TIL_JPEG_COMPRESSION_MESSES_WITH_RGB_VALUES}

music.png (Misc 300)

赤のみで値を文字にすると、同じ文字列の繰り返しになっている。
緑のみ、青のみも同様。繰り返しなくして、連結してみる。

from PIL import Image

img = Image.open('music.png').convert('RGB')
w, h = img.size

r_str = ''
g_str = ''
b_str = ''
for y in range(0, h):
    for x in range(0, w):
        r, g, b = img.getpixel((x, y))
        r_str += chr(r)
        g_str += chr(g)
        b_str += chr(b)

s = r_str[:32] + g_str[:38] + b_str[:66]
print s

結果は以下の通り。

(t<<3)*[8/9,1,9/8,6/5,4/3,3/2,0][[0xd2d2c7,0xce4087,0xca32c7,0x8e4008][t>>14&3.1]>>(0x3dbe4687>>((t>>10&15)>9?18:t>>10&15)*3&7.1)*3&7.1]

これで検索してみると、以下のページが見つかった。

https://gist.github.com/djcsdy/2875542

値はほぼ同じ。値を入れ替え、ブラウザのデベロッパーツールでConsoleから実行してみたが、うまくいかない。上記のページの値で実行し直し、BASE64データを取得する。デコードして保存するとwavファイルになった。
この曲名当ての問題のようなので、アプリに音楽を聞かせ、曲名が「Never Gonna Give You Up」であることがわかった。

RITSEC{never_gonna_give_you_up}

I am a Stegosaurus (Forensics 250)

TweakPNGで開こうとすると、以下のようなメッセージあり。

Incorrect crc for IHDR chunk (is 93cf1eca, should be 01aae416)

IHDRチャンクのCRCを正しく修正する。画像の下の方にフラグが書いてある。

RITSEC{th1nk_0uts1d3_th3_b0x}

Space Force (Web 100)

' or 1=1 # と入力すると、全データが表示される。Shipname「LbtebKe6yrU8vEnx」のCaptainがフラグだった。

RITSEC{hey_there_h4v3_s0me_point$_3ny2Lx}

CictroHash (Crypto 150)

PDFを読み、ハッシュ処理をスクリプトにする。4バイトのハッシュということもあり、短い文字で衝突しそうなので、ブルートフォースする。

import itertools

def lol(val):
    s = bin(val)[2:].zfill(8)
    s = s[1:] + s[0]
    return int(s, 2)

def rol(val):
    s = bin(val)[2:].zfill(8)
    s = s[-1] + s[:-1]
    return int(s, 2)

def alpha(w):
    return [w[1], w[0]]

def beta(w):
    w[0][0] ^= w[1][3]
    w[0][1] ^= w[1][2]
    w[0][2] ^= w[1][1]
    w[0][3] ^= w[1][0]
    return w

def gamma(w):
    new_w = [[0, 0, 0, 0], [0, 0, 0, 0]]
    new_w[0][3] = w[0][0]
    new_w[1][2] = w[0][1]
    new_w[1][3] = w[0][2]
    new_w[1][1] = w[0][3]
    new_w[0][1] = w[1][0]
    new_w[1][0] = w[1][1]
    new_w[0][2] = w[1][2]
    new_w[0][0] = w[1][3]
    return new_w

def delta(w):
    w[0][0] = lol(w[0][0])
    w[1][0] = lol(w[1][0])
    w[0][2] = lol(w[0][2])
    w[1][2] = lol(w[1][2])
    w[0][1] = rol(w[0][1])
    w[1][1] = rol(w[1][1])
    w[0][3] = rol(w[0][3])
    w[1][3] = rol(w[1][3])
    return w

def round(w):
    w = alpha(w)
    w = beta(w)
    w = gamma(w)
    w = delta(w)
    return w

def f(w):
    for i in range(50):
        w = round(w)
    return w

def xor_pre(w, p):
    for i in  range(4):
        w[0][i] ^= p[i]
    return w

def str_to_blocks(s):
    while True:
        if len(s) % 4 == 0:
            break
        s += '\x00'
    blocks = []
    for i in range(len(s) / 4):
        block = []
        for j in range(4):
            block.append(ord(s[i*4+j]))
        blocks.append(block)
    return blocks

def block_to_hex(block):
    h = ''
    for code in block:
        h += ('%x' % code).zfill(2)
    return h

def get_hash(msg):
    w = [[31, 56, 156, 167], [38, 240, 174, 248]]
    blocks = str_to_blocks(msg)
    for block in blocks:
        w = xor_pre(w, block)
        w = f(w)
    return block_to_hex(w[1])

chars = ''.join([chr(i) for i in range(33, 127)])
dic = {}
found = False
for i in range(1, 6):
    for c in itertools.product(chars, repeat=i):
        text = ''.join(c)
        print text
        h = get_hash(text)
        if h in dic:
            found = True
            print 'Found!'
            print '1:', text
            print '2:', dic[h]
            print 'hash:', h
            break
        else:
            dic[h] = text
    if found:
        break

実行結果は以下の通り。

Found!
1: !!!U
2: tt!
hash: 1ffb77c0
$ curl -X POST http://fun.ritsec.club:8003/checkCollision --header "Content-Type: application/json" --data '{"str1": "!!!U", "str2": "tt!"}'
{
  "flag": "RITSEC{I_am_th3_gr3@t3st_h@XOR_3v@}", 
  "success": true
}
RITSEC{I_am_th3_gr3@t3st_h@XOR_3v@}

Nobody uses the eggplant emoji (Crypto 200)

絵文字を使った換字式暗号だとわかった。まず絵文字を半角英字に置き換える。

ABCDEFCGHAIJCDEFCKLMCNELGHDCEBCGHMCBOKPCBALQGCDEFCRFQGCKIQNMLCGHMQMCGHLMMCSFMQGAEIQTCNHKGCAQCDEFCIKRMUCNHKGCAQCDEFLCSFMQGUCNHKGCAQCGHMCKALCQVMMWCXMOEYAGDCEBCKICFIOKWMICQNKOOENTCDEFLCBOKPCAQZCKBLAYKI_EL_MFLEVMKI_QNKOOEN_NEN_GHMLMQ_K_WABBMLMIYMC

quipqiup(https://quipqiup.com/)で復号してみるが、ダメだった。以下のように最後の_をスペースに置き換えて試す。

ABCDEFCGHAIJCDEFCKLMCNELGHDCEBCGHMCBOKPCBALQGCDEFCRFQGCKIQNMLCGHMQMCGHLMMCSFMQGAEIQTCNHKGCAQCDEFCIKRMUCNHKGCAQCDEFLCSFMQGUCNHKGCAQCGHMCKALCQVMMWCXMOEYAGDCEBCKICFIOKWMICQNKOOENTCDEFLCBOKPCAQZCKBLAYKI EL MFLEVMKI QNKOOEN NEN GHMLMQ K WABBMLMIYMC

結果以下のようになった。

IFS?OUSJHIN?S?OUSARESBORJH?SOFSJHESF?A?SFIRKJS?OUS?UKJSANKBERSJHEKESJHREES?UEKJIONK?SBHAJSIKS?OUSNA?E?SBHAJSIKS?OURS?UEKJ?SBHAJSIKSJHESAIRSKPEEDS?E?OCIJ?SOFSANSUN?ADENSKBA??OB?S?OURSF?A?SIK?SAFRICAN OR EUROPEAN KBA??OB BOB JHEREK A DIFFERENCES

"AFRICAN", "EUROPEAN"や"A DIFFERNCE"は合っていそう。最後は複数形はおかしいので、Sは保留。ここから変換を試しながら、穴を埋めていく。最終コードは以下の通り。

import string

enc = 'ABCDEFCGHAIJCDEFCKLMCNELGHDCEBCGHMCBOKPCBALQGCDEFCRFQGCKIQNMLCGHMQMCGHLMMCSFMQGAEIQTCNHKGCAQCDEFCIKRMUCNHKGCAQCDEFLCSFMQGUCNHKGCAQCGHMCKALCQVMMWCXMOEYAGDCEBCKICFIOKWMICQNKOOENTCDEFLCBOKPCAQZCKBLAYKI_EL_MFLEVMKI_QNKOOEN_NEN_GHMLMQ_K_WABBMLMIYMC'

cipher = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
plain  = 'if youthnkarewlgsmq.,pdvc:'

table = string.maketrans(cipher, plain)
msg = enc.translate(table)
print msg

復号結果は以下の通り。

if you think you are worthy of the flag first you must answer these three questions. what is you name, what is your quest, what is the air speed velocity of an unladen swallow. your flag is: african_or_european_swallow_wow_theres_a_difference
RITSEC{african_or_european_swallow_wow_theres_a_difference}

Who drew on my program? (Crypto 350)

AES CBCモードなので、以下のようになる。

[平文1ブロック目] ^ IV                  --(暗号化)--> [暗号文1ブロック目]
[平文2ブロック目] ^ [暗号文1ブロック目] --(暗号化)--> [暗号文2ブロック目]

平文全体、暗号文1ブロック目の一部と暗号文2ブロック目はわかっている。鍵が14バイトわかっているので、この条件を満たすよう残りはブルートフォースで一致するものを求める。最終的にIVを求めると、フラグがわかる。

from Crypto.Cipher import AES
import binascii
import string

def str_xor(s1, s2):
    return ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(s1, s2))

def decrypt(ciphertext, passphrase, IV):
    aes = AES.new(passphrase, AES.MODE_CBC, IV)
    return aes.decrypt(ciphertext)

pre_KEY = '9aF738g9AkI112'
plain = 'The message is protected by AES!'
cipher = '9exxxxxxxxxxxxxxxxxxxxxxxxxx436a808e200a54806b0e94fb9633db9d67f0'

found = False
for i in range(32, 127):
    for j in range(32, 127):
        KEY = pre_KEY + chr(i) + chr(j)
        tmp_cipher = cipher.replace('x', '0')
        tmp_cipher = binascii.unhexlify(tmp_cipher)
        dec = decrypt(tmp_cipher, KEY, '0' * 16)
        if dec[16] == plain[16] and dec[30:] == plain[30:]:
            found = True
            break
    if found:
        break

aes = AES.new(KEY, AES.MODE_ECB)
cipher2 = binascii.unhexlify(cipher[32:])
cipher1 = str_xor(aes.decrypt(cipher2), plain[16:])
IV = str_xor(aes.decrypt(cipher1), plain[:16])
print IV
RITSEC{b4dcbc#g}

Square CTF 2018 Writeup

この大会は2018/11/9 4:00(JST)~2018/11/15 4:00(JST)に開催されました。
今回もチームで参戦。結果は 70818点で595チーム中21位でした。
自分で解けた問題をWriteupとして書いておきます。

C2: flipping bits (cryptology)

共通のnとe1,c1とe2,c2がわかっているので、Common Modules Attackで復号できる。

import gmpy

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

c1 = 13981765388145083997703333682243956434148306954774120760845671024723583618341148528952063316653588928138430524040717841543528568326674293677228449651281422762216853098529425814740156575513620513245005576508982103360592761380293006244528169193632346512170599896471850340765607466109228426538780591853882736654
c2 = 79459949016924442856959059325390894723232586275925931898929445938338123216278271333902062872565058205136627757713051954083968874644581902371182266588247653857616029881453100387797111559677392017415298580136496204898016797180386402171968931958365160589774450964944023720256848731202333789801071962338635072065
e1 = 13
e2 = 15
n = 103109065902334620226101162008793963504256027939117020091876799039690801944735604259018655534860183205031069083254290258577291605287053538752280231959857465853228851714786887294961873006234153079187216285516823832102424110934062954272346111907571393964363630079343598511602013316604641904852018969178919051627
 
m = commom_modules_attack(c1, c2, e1, e2, n)
flag = ('%x' % m).decode('hex')
print flag
flag-54d3db5c1efcd7afa579c37bcb560ae0

C3: shredded (image processing)

とりあえずそのままImageMagicで連結してみる。QRコードっぽいことだけわかる。並び替えながら、正しいのを探す。

$ convert +append 0.png 12.png 13.png 5.png 6.png 25.png 16.png 2.png 15.png 26.png 3.png 20.png 10.png 21.png 4.png 19.png 7.png 8.png 1.png 22.png 23.png 24.png 18.png 14.png 11.png 17.png 9.png qr.png

f:id:satou-y:20181115195708p:plain
この場合だめだった。まだ何パターンもあるので、わかっている部分以外で総当たりする。

わからない部分:
・左のPositionの3列の黒の箇所
・真ん中5列(ただし、Timingが黒白黒白黒になることからさらに制限できる)
・右のPositionの3列の黒の箇所
from PIL import Image
import itertools
from pyzbar.pyzbar import decode

FILE_FORMAT = 'shredded/%d.png'
WIDTH = 10
HEIGHT = 297

def makeQR(ary):
    output_img = Image.new('RGB', (WIDTH*27, HEIGHT), (255, 255, 255))

    for i in range(27):
        input_img = Image.open(FILE_FORMAT % ary[i])
        output_img.paste(input_img, (WIDTH*i, 0))

    output_img.save('flag.png')

left = [2, 16, 25]
mid_black = [4, 20, 21]
mid_white = [10, 19]
right = [22, 23, 24]

found = False
for a in itertools.permutations(left):
    for b in itertools.permutations(mid_black):
        for c in itertools.permutations(mid_white):
            for d in itertools.permutations(right):
                ary = [0, 12, 13, 5, 6]
                ary += a
                ary += [15, 26, 3]
                ary += [b[0]]
                ary += [c[0]]
                ary += [b[1]]
                ary += [c[1]]
                ary += [b[2]]
                ary += [7, 8, 1]
                ary += d
                ary += [18, 14, 11, 17, 9]
                makeQR(ary)
                try:
                    data = decode(Image.open('flag.png'))
                    print data[0][0].decode('utf-8', 'ignore')
                    print ary
                    found = True
                    break
                except:
                    continue
            if found:
                break
        if found:
            break
    if found:
        break

実行結果は以下の通り。

GOOD JOB. FLAG-80AD8BCF79
[0, 12, 13, 5, 6, 16, 25, 2, 15, 26, 3, 4, 19, 21, 10, 20, 7, 8, 1, 22, 24, 23, 18, 14, 11, 17, 9]
FLAG-80AD8BCF79

C4: leaky power (cryptology)

AESの相関電力解析 (CPA : Correlation Power Analysis)の問題。
https://wiki.newae.com/Tutorial_B6_Breaking_AES_(Manual_CPA_Attack)を参考にして、鍵を推測しjweを復号するコードを実行する。

import numpy as np
from jwcrypto.common import base64url_decode, base64url_encode
from jwcrypto import jwk, jwe

HW = [bin(n).count("1") for n in range(0,256)]

sbox=(
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16)

def intermediate(pt, keyguess):
    return sbox[pt ^ keyguess]

traces = np.load('powertraces.npy')
pt = np.load('plaintexts.npy')

numtraces = np.shape(traces)[0]-1
numpoint = np.shape(traces)[1]

#Use less than the maximum traces by setting numtraces to something
#numtraces = 15

bestguess = [0]*16
#Set 16 to something lower (like 1) to only go through a single subkey & save time!
for bnum in range(0, 16):
    cpaoutput = [0]*256
    maxcpa = [0]*256
    for kguess in range(0, 256):
        print "Subkey %2d, hyp = %02x: "%(bnum, kguess),


        #Initialize arrays & variables to zero
        sumnum = np.zeros(numpoint)
        sumden1 = np.zeros(numpoint)
        sumden2 = np.zeros(numpoint)

        hyp = np.zeros(numtraces)
        for tnum in range(0, numtraces):
            hyp[tnum] = HW[intermediate(pt[tnum][bnum], kguess)]


        #Mean of hypothesis
        meanh = np.mean(hyp, dtype=np.float64)

        #Mean of all points in trace
        meant = np.mean(traces, axis=0, dtype=np.float64)

        #For each trace, do the following
        for tnum in range(0, numtraces):
            hdiff = (hyp[tnum] - meanh)
            tdiff = traces[tnum,:] - meant

            sumnum = sumnum + (hdiff*tdiff)
            sumden1 = sumden1 + hdiff*hdiff 
            sumden2 = sumden2 + tdiff*tdiff

        cpaoutput[kguess] = sumnum / np.sqrt( sumden1 * sumden2 )
        maxcpa[kguess] = max(abs(cpaoutput[kguess]))

        print maxcpa[kguess]

    #Find maximum value of key
    bestguess[bnum] = np.argmax(maxcpa)

print "Best Key Guess: "
key = ''
for b in bestguess:
    key += "%02x" % b
    print "%02x" % b,
print ''

with open('instructions_corrected.jwe', 'r') as f:
    enc = f.read()

key = jwk.JWK(kty = 'oct', k = base64url_encode(key.decode('hex')))

jwetoken = jwe.JWE()
jwetoken.deserialize(enc)
jwetoken.decrypt(key)
payload = jwetoken.payload
print payload

実行結果は以下のとおり。

    :
Best Key Guess:
d2 de a0 57 d1 14 5f 45 67 96 96 60 24 a7 03 b2
CONFIDENTIAL

To disable C4, you will need:
- 6 bits of Dragon Sumac
- 1 nibble of Winter Spice
- 1 byte of Drake Cardamom
- 1 flag with value flag-e2f27bac480a7857de45
- 2 diskfulls of Tundra Chives
- 5 forks

Grind the Dragon Sumac in a cup, making sure you don't break the cup as it's probably a delicate cup. Add a sprinkle of
liquid ice to turn it into a cream-like paste, then add the Winter Spice, first almost everything, then the last tiny
remnants.

Fill a pan with elemental water, add the mixture and cool it down with how cool you are, then bring the mixture
to a boil. Let it cool down to the body temperature of a reptile before adding the Drake Cardamom and Tundra Chives,
all at once of one, then half at a time of the other.

Bring everything back to a boil, turn of the heat, mix with the forks and let everything cool down. If you
touch the liquid and it burns you, it hasn't cooled down enough.

Whisk the mixture heavily to aerate it. Stop when it's frothy.

Drinking the potion will disable C4.

note: A small, but very cold amount is needed for the potion to be effective. Mixing it in a milkshake could work, but
be wary of brain freeze.
flag-e2f27bac480a7857de45

C5: de-anonymization (gdpr)

datasetからYakubovicsに関する情報を探す。

■Yakubovicsで検索
1.csv
email,job_title,income
eyakubovics9t@nih.gov,Captain,96605

2.csv
email,state
eyakubovics9t@nih.gov,Florida

■96605で検索
4.csv
income,state,street,postal
96605,Florida,4 Magdeline,33421

■"4 Magdeline"で検索
3.csv
ssn_last_four,state,street
4484,Florida,4 Magdeline

5.csv
first_name,street
Elyssa,4 Magdeline

Reset Passwordで以下入力

- First Name: Elyssa
- Last Name: Yakubovics
- Email Address: eyakubovics9t@nih.gov
- What are the last four digits of your SSN?: 4484
- What is the name of the street you grew up on?: 4 Magdeline

Reset Passwordの画面に移るので、HTMLソースを見ると、Current Passwordがフラグになっていた。

<input name='current_password' type='password' value='flag-5ed43dc6356b2b68c689422769952b82' readonly />
flag-5ed43dc6356b2b68c689422769952b82

C8: captcha (math)

HTMLソースを見ると、数式は以下のようになっている。

ggggmt o CW j gggs j gZ j ZWW I ggm o ggp o gn I mtWW o gR I hWWW I EWW o sWW I ggggR o mW j gn o mWW o ggZ j pW I ZWW I gggggp I RW o ggm I sW j gm I mtWWW o ggs o ggs o ZW o gE j gmt j pWWWW o sWW o hW j gC j ggC j EW I gs I gR o pWWWWWWW o gggh o mtW o gggE I EW I EW o RWW j gs I gp j gm o mtWWWWW

表示されている文字はフォント定義により異なる文字を表している。

1回アクセスしたときのbase64文字列からttfを作成しておき、対応する数字のフォントデータを辞書として記憶しておく。それを元に対応する数式に変換し、計算して答える。

import requests
import re
from fontTools.ttLib import TTFont
from fontTools.pens.recordingPen import RecordingPen

def make_formula(records_pen, cmap, s):
    f = s
    for code in cmap:
        glyph_name = cmap[code]
        char = glyph_set[glyph_name]
        recording_pen = RecordingPen()
        char.draw(recording_pen)
        idx = records_pen.index(recording_pen.value)
        f = f.replace(chr(code), chars[idx])
    return f

r = requests.get('https://hidden-island-93990.squarectf.com/ea6c95c6d0ff24545cad')
body = r.text

pattern = 'base64,(.+)\'\);\}.+<p>(.+)</p>.+\"token\" value=\"(\d+)\"'
m = re.search(pattern, body)
b64_ttf = m.group(1)
sign = m.group(2)
token = m.group(3)

with open('tmp.ttf', 'wb') as f:
    f.write(b64_ttf.decode('base64'))

chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '(', ')', '+', '-', '*']

f = TTFont('base.ttf')
glyph_set = f.getGlyphSet()
cmap = f.getBestCmap()

base_list = [102, 120, 87, 111, 113, 65, 70, 89, 119, 107, 78, 98, 115, 81, 100]
records_pen = []
for code in base_list:
    glyph_name = cmap[code]
    char = glyph_set[glyph_name]
    recording_pen = RecordingPen()
    char.draw(recording_pen)
    records_pen.append(recording_pen.value)

f = TTFont('tmp.ttf')
glyph_set = f.getGlyphSet()
cmap = f.getBestCmap()
formula = make_formula(records_pen, cmap, sign)
print formula
ans = eval(formula)
print ans

param = {'token': token, 'answer': str(ans)}
r = requests.post('https://hidden-island-93990.squarectf.com/ea6c95c6d0ff24545cad', data = param)
print r.text

実行結果は以下の通り。

(((((3 * (2 - 10)) * 8) + ((((3 - (10 - 9)) * 6) * ((7 + 2) - 5)) + (((9 + ((6 - 7) * (8 - (2 + (9 + 4))))) * ((2 - 6) * ((4 - 3) * 8))) - (1 - 1)))) * (((7 + 1) - 10) * (3 + 3))) - ((2 + 8) * ((((1 - 4) + (4 * (7 - (4 - 6)))) * ((3 * ((1 - 1) + 5)) + ((2 * (5 * 2)) + 1))) - ((1 * (7 * 10)) * 9))))
2292
<html><body><p>If you can see this then you have solved the CAPTCHA.</p><p>If you have solved the CAPTCHA then you are a Charvise.</p><p>If you are a Charvise then you are welcome on Charvis.</p><p>If you are welcome on Charvis then:</p><ol><li>You can disable this system using flag-a76013167fd4c04e3134</li><li><p>You should be given useful information.</p><p>If you should be given useful information then this informs you that there are two layers of defense left, and the last one is trivial.</p></body></html>
flag-a76013167fd4c04e3134

HCTF 2018 Writeup

この大会は2018/11/9 21:00(JST)~2018/11/11 21:00(JST)に開催されました。
今回もチームで参戦。結果は 916.6点で1466チーム中55位でした。
自分で解けた問題をWriteupとして書いておきます。

xor game (Crypto)

xorの鍵が不明で、その鍵を突き止める問題。
暗号化データをBase64デコードして、XOR Cracker(https://wiremask.eu/tools/xor-cracker/)にかける。
その結果を参考にして調整しながら、鍵を求める。

from Crypto.Util.strxor import strxor
import base64

def dec(data, key):
    key = (key * (len(data) / len(key) + 1))[:len(data)]
    return strxor(data, key)

with open('cipher.txt', 'r') as f:
    data = f.read()

b64enc = base64.b64decode(data)
with open('cipher', 'wb') as f:
    f.write(b64enc)

## arrange ##
print dec(b64enc, 'xor_is_interesting!@#')

これを実行すると、きちんと復号できていることが分かる。

Life, thin and light-off time and time again
Frivolous tireless
one
I heard the echo, from the valleys and the heart
Open to the lonely soul of sickle harvesting
Repeat outrightly, but also repeat the well-being of
Eventually swaying in the desert oasis
I believe I am
Born as the bright summer flowers
Do not withered undefeated fiery demon rule
Heart rate and breathing to bear the load of the cumbersome
Bored
Two
I heard the music, from the moon and carcass
Auxiliary extreme aestheticism bait to capture misty
Filling the intense life, but also filling the pure
There are always memories throughout the earth
I believe I am
Died as the quiet beauty of autumn leaves
Sheng is not chaos, smoke gesture
Even wilt also retained bone proudly Qing Feng muscle
Occult
Three
I hear love, I believe in love
Love is a pool of struggling blue-green algae
As desolate micro-burst of wind
Bleeding through my veins
Years stationed in the belief
Four
I believe that all can hear
Even anticipate discrete, I met the other their own
Some can not grasp the moment
Left to the East to go West, the dead must not return to nowhere
See, I wear Zan Flowers on my head, in full bloom along the way all the way
Frequently missed some, but also deeply moved by wind, frost, snow or rain
Five
Prajna Paramita, soon as soon as
life be beautiful like summer flowers and death like autumn leaves
Also care about what has
hctf{xor_is_interesting!@#}

xor?rsa (Crypto)

$ nc rsa.2018.hctf.io 10086
Welcome to flag getting system
give me your token > 2jrCI4AslZF63Or3g0BCLzKoe0rXGTYZ
n=25275261816080723577383625252321605813293020177515415346273626867915569138289688717413356558193996845476305528125832070604557462428746989292112808118629779995853500422163753237317584703994425361069816700082487715542167171713699666726887235382197073068733120714572524136047731843566904816054447149749558432678912765881098765528412138624603442724195067422316458776382746088522690538448161527517375561342031327658770828995835520042246472144202767031798115768655362628916139166585435054566298105440647015024307766333320215539234184515515996807842360812897141090870808136031078799184603768964924810402365848053621310874371
c1=11165085713561189003280953910551795650069641727697255665204270392356119682302194809551085381412459067137708791048033212887178854783178521082866084322294736924387003502539404006446446960913768668751546334339464672969536040183161881553997971542237143014930493145940738807334373127733697348176468962798526646137597541643569114699950367076718329916819155968155932149333397328469867074678751467552042630972042728542318490144115620371495906795551552788089985932610796897659179997445268713553678805380667254387708279651188394366215609009582176348204515365158369592961753687386325076074438007962972262423575684012468736502784
c2=3692619448894086202620380631082268435018623838407343923441472120868415709925497517010788618542797329154197746377107382855787882364201552243204454392390226061058770734934557129438642712750088115325177185876919748092262061559995643597615670605259279309223337082989077402836087239826642767218219888576126501421187433421552115165902886235999884421474474839807257919154091416829866712451600989516801908414284252064861856712338940516499148321453909303775761944595561080379704862572754567977567513085826683733515940571450314778688781004557900332788704156673505934867551765430160894036472529704734924702982507494191016739987
now give me you answer

それぞれパラメータは以下のようになっている。

p = 1024ビットランダム素数
q = 1024ビットランダム素数
n = p * q
e = 5
kbits = nのビット数 // (2*e*e)
m1 = nのビット数のランダム整数
m2 = m1 ^ kbitsのランダム整数
c1 = pow(m1, e, n)
c2 = pow(m2, e, n)

m1とm2を求める問題。kbitsが上記の定義なので、m1とm2の差は小さく、Coppersmith's Short Pad Attackでそれぞれ復号できる。

# solve.sage
import socket

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

def short_pad_attack(c1, c2, e, n):
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))

    g1 = x^e - c1
    g2 = (x+y)^e - c2

    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)

    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()

    kbits = n.nbits()//(2*e*e)
    diff = h.small_roots(X=2^kbits, beta=0.5)[0]

    return diff

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]

token = '2jrCI4AslZF63Or3g0BCLzKoe0rXGTYZ'

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('rsa.2018.hctf.io', 10086))

data = recvuntil(s, '> ')
print data + token
s.sendall(token + '\n')
data = recvuntil(s, '\n').strip()
print data
n = Integer(data[2:])
e = 5
data = recvuntil(s, '\n').strip()
print data
c1 = Integer(data[3:])
data = recvuntil(s, '\n').strip()
print data
c2 = Integer(data[3:])

diff = short_pad_attack(c1, c2, e, n)
m1 = related_message_attack(c1, c2, diff, e, n)
m2 = m1 + diff

data = recvuntil(s, '\n').strip()
print data
print m1
s.sendall(str(m1) + '\n')
print m2
s.sendall(str(m2) + '\n')
data = s.recv(1024)
print data
hctf{c81d20647f1ff3d55fc25626cd29ec4d95a3d66977973569a5a7a1b2487a1e9a}

SECCON 2018 Online CTF Writeup

この大会は2018/10/27 15:00(JST)~2018/10/28 15:00(JST)に開催されました。
今回もチームで参戦。結果は 4245点で653チーム中7位でした。
自分で解けた問題をWriteupとして書いておきます。

mnemonic (Crypto)

$ cat mnemonic.txt
{
    "japanese": [
	[
	    "d3a02b9706507552f0e70709f1d4921275204365b4995feae1d949fb59c663cc",
	    "ふじみ あさひ みのう いっち いがく とない はづき ますく いせえび たれんと おとしもの おどろかす ことし おくりがな ちょうし ちきゅう さんきゃく こんとん せつだん ちしき ぬいくぎ まんなか たんい そっと",
	    "338c161dbdb47c570d5d75d5936e6a32178adde370b6774d40d97a51835d7fec88f859e0a6660891fc7758d451d744d5d3b1a1ebd1123e41d62d5a1550156b1f"
	],
	[
	    "dfc9708ac4b4e7f67be6b8e33486482cb363e81967a1569c6fd888b088046f7c",
	    "ほんやく ごうきゅう おさめる たこやき ごかん れいぎ やせる ふるい まんなか てんない だんろ さうな きぼう よくぼう しのぐ よけい こんき みうち らくご いわかん いこく あたためる のはら たぶん",
	    "bdadda5bbff97eb4fda0f11c7141bc3ce3de0fef0b2e4c47900858cec639c10187aee4695b1ba462b1dd34b170b62801e68c270b93af62629f4964947a620ed9"
	],
	[
	    "c0f...",
	    "??? とかす なおす よけい ちいさい さんらん けむり ていど かがく とかす そあく きあい ぶどう こうどう ねみみ にあう ねんぐ ひねる おまいり いちじ ぎゅうにく みりょく ろしゅつ あつめる",
	    "e9a..."
	],
    ],
    "flag": "SECCON{md5(c0f...)}"
}

日本語の名称やタイトルで調べると、タイトル通り、mnemonicの問題。
日本語のリストは以下にあった。

https://github.com/bitcoin/bips/blob/master/bip-0039/japanese.txt

さらに何かよいライブラリがないか調べると、python-mnemonicがあったので、試しにこれを使ってみる。

https://github.com/trezor/python-mnemonic/tree/master/mnemonic

ここにもワードリストがある。試すときに、コード中の最後の方にある部分をenglishからjapaneseに変える。

m = Mnemonic('english')
    ↓
m = Mnemonic('japanese')
$ python mnemonic.py d3a02b9706507552f0e70709f1d4921275204365b4995feae1d949fb59c663cc
ふじみ あさひ みのう いっち いがく とない はづき ますく いせえび たれんと おとしもの おどろかす ことし おくりがな ちょうし ちきゅう さんきゃく こんとん せつだん ちしき ぬいくぎ まんなか たんい そっと
$ python mnemonic.py dfc9708ac4b4e7f67be6b8e33486482cb363e81967a1569c6fd888b088046f7c
ほんやく ごうきゅう おさめる たこやき ごかん れいぎ やせる ふるい まんなか てんない だんろ さうな きぼう よくぼう しのぐ よけい こんき みうち らくご いわかん いこく あたためる のはら たぶん

問題の結果と一致する。この中のコードを見て、逆算する。ただし、1つ目のワードがわからないので、そこはブルートフォースする。
日本語の処理が面倒だったので、リストを見ながら、インデックス(何行目か)を抽出する。その結果を含めたコードは以下の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import hashlib

def decrypt(val):
    idxes = [val, 1333, 1376, 1953, 1173, 777, 570, 1262, 337, 1333, 995, 375,
        1706, 616, 1485, 1404, 1495, 1644, 297, 91, 444, 1844, 2030, 24]

    b = ''
    for idx in idxes:
        b += bin(idx)[2:].zfill(11)
    b = b[:-8]

    data = ''
    for i in range(0, len(b), 8):
        data += chr(int(b[i:i+8], 2))

    return data.encode('hex')

pre = 'c0f'

for i in range(2**11):
    h = decrypt(i)
    if h.startswith(pre):
        print 'val =', str(i)
        print 'h =', h
        break

flag = 'SECCON{' + hashlib.md5(h).hexdigest() + '}'
print flag
SECCON{cda2cb1742d1b6fc21d05c879c263eec}

でもよく考えたら、ブルートフォースする必要はなかった。11bitごとに最初の単語がきまるので、適当な単語で復号して、先頭3文字をc0fにするだけでよかった。

BSides Delhi CTF 2018 Writeup

この大会は2018/10/25 20:00(JST)~2018/10/26 20:00(JST)に開催されました。
今回もチームで参戦。結果は 1376点で426チーム中23位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity Check (Welcome 1)

freenodeで#bi0s-ctfチャネルに入る。

20:44 *topic :  Official channel for BSides CTF  | Flag for sanity check: flag{w3lc0m3}  | CTF starts at 11:30 UTC and runs for 24 hours
flag{w3lc0m3}

RSA-Baby (Crypto 100)

modulusのうちそれぞれペアで公約数を出してみる。1以外の公約数があったので、素因数分解できた。素因数分解できたnに対応するeは素数ではない。
例えば、1つ目は以下のようになる。

114194 = 2 * 57097

RSA暗号はm^e%n=cなので、m^(e1*e2)%n=c、(m^e1)^e2%n=cとなる。
一旦57097で復号する。その後2乗根で、フラグが得られるかどうか試してみる。

from Crypto.Util.number import *
import itertools
import gmpy

def egcd(a, b):
    x,y, u,v = 0,1, 1,0
    while a != 0:
        q, r = b//a, b%a
        m, n = x-u*q, y-v*q
        b,a, x,y, u,v = a,r, u,v, m,n
    gcd = b
    return gcd, x, y

modulus_list = [143786356117385195355522728814418684024129402954309769186869633376407480449846714776247533950484109173163811708549269029920405450237443197994941951104068001708682945191370596050916441792714228818475059839352105948003874426539429621408867171203559281132589926504992702401428910240117807627890055235377744541913L,
 73988726804584255779346831019194873108586184186524793132656027600961771331094234332693404730437468912329694216269372797532334390363774803642809945268154324370355113538927414351037561899998734391507272602074924837440885467211134022878597523920836541794820777951492188067045604789153534513271406458984968338509L,
 95666403279611361071535593067846981517930129087906362381453835849857496766736720885263927273295086034390557353492037703154353541274448884795437287235560639118986397838850340017834752502157881329960725771502503917735194236743345777337851076649842634506339513864285786698870866229339372558162315435127197444193L, 
 119235191922699211973494433973985286182951917872084464216722572875998345005104112625024274855529546680909781406076412741844254205002739352725207590519921992295941563460138887173402493503653397592300336588721082590464192875253265214253650991510709511154297580284525736720396804660126786258245028204861220690641L]

e = [114194L, 130478L, 122694L, 79874L]

ciphertext = ['0c55bc89e3773d8e378121eced4f9300103a8696bc3f9a1542c5b1539442ca5de03a40ad564ab5c2e764b2f946058ec220abf20afc271896ff4ca1f4a2dd227405f221de51e097d6b9f270c4561cd25596e96efd7de1a0e65d37cbf6a73c62a7e323f48450b9dc75e3e738ec1c7e1ae9fc808da8c476e72aea9155125b815653', '67caf9720696b1d0d589f053bb00ebe42b7b26fed38acb4012d29ddc55cd53da8398f042f22987453bdfa2ee8fb35ff121f81e96137995a8ca4daa1fbd88af3fd29138853d5fe98f9b983f67d6fd2b7ff6650228479ca6cac1d49572d28f01a659892b0799ca8202031a1ab37656331470d3ea5f221cc948636c1027bb6dd10f', '65e1cffe93ebccd49a9d14c01b2583a5d5e3140bf38a768833aa494d2d879a2934dbc10a843ec834e9ade286824e68879cb09ac9bd67afd7318b74955e9aa66df5740e6dcc26ccc787f0b415bdc80c6468421c4d4ce615fa3d25350940c5004e9b480c86faebc31e809725a9a868c94e9f1eaac567b4672fe395a7b205775883', '23108bb7f35d12b69bbe5e649ff47fb802b68f22045c484805040a3f4f8669acde8b04daba71190154aef4be9a0eafdebe31b5f96e8b01b5085f502fc0e12a326cc4d867f5317ac12bf16607765d99708934c35c4b9404747f69988ea7d3f4d8022cdfd81ada3aedb22d110db4aa81038aa151c9a4dbb5651757dc092b70b84d']

for ns in list(itertools.combinations(modulus_list, 2)):
    gcd = egcd(ns[0], ns[1])
    if gcd[0] > 1:
        n = ns[0]
        p = gcd[0]
        q = n / p

idx = modulus_list.index(n)
print 'index =', idx

c = int(ciphertext[idx], 16)
phi = (p - 1) * (q - 1)
d = inverse(e[idx]/2, phi)
m_tmp = pow(c, d, n)

m = gmpy.root(m_tmp, 2)[0]
flag = long_to_bytes(m)
print flag
flag{Congratzzz_y0u_kn0w_ext3nded_GCD_WOw!!}

pyQueue (Crypto 150)

keyが少しずつ後ろにずれて、暗号化していて、以下のようなイメージになっている。

1ブロック目:key[1:] + ?
2ブロック目:key[2:] + ? + ?
3ブロック目:key[3:] + ? + ? + ?

MAC: C1 ^ C2 ^ C3 ^ (key[4:] + ? + ? + ? + ?)

最終的なMACがわかっているので、逆算して鍵を割り出していけば、復号できる。鍵の最初の1バイトに対して、ブルートフォース

from Crypto.Util.number import *
from Crypto.Cipher import AES

def is_printable(s):
    for c in s:
        code = ord(c)
        if code > 126:
             return False
    return True

def unpad(s):
    return s[:-ord(s[-1])]

with open('ci.pher.text', 'r') as f:
    data = f.read().split(':')

MAC = int(data[0])
ct = data[1]

ct1 = ct[:32]
ct2 = ct[32:64]
ct3 = ct[64:]
ct = [ct1, ct2, ct3]
key = int(ct1, 16) ^ int(ct2, 16) ^ int(ct3, 16) ^ MAC
key = long_to_bytes(key)

flag = ''
for i in range(len(ct)):
    for code in range(256):
        try_key = chr(code) + key[:-1]
        cipher = AES.new(try_key, AES.MODE_ECB)
        pt = cipher.decrypt(ct[2-i].decode('hex'))
        if is_printable(pt):
            flag = pt + flag
            key = try_key
            break

flag = unpad(flag)
print flag
flag{H4il_bUggi3s!_qu3u3_k3y_2_h0t_4_w4rmUp}

RSA-reloaded (Crypto 200)

rとsは比較的近い素数と考えられるため、Fermat法で素因数分解する。さらにx+f1の次の素数がrになることからxの範囲を探し、2次方程式を解くことによって、p,qを求め復号する。

q%p = x
q = p*k+x

まずはk=1で考える。
⇒p * (p+x) = N1 ⇒ p**2 + x*p = N1
⇒該当するpが見つかる。

最終的なコードは以下の通り。

from Crypto.PublicKey import RSA
from Crypto.Util.number import *
import gmpy
from z3 import *

def isqrt(n):
    x = n
    y = (x + n // x) // 2
    while y < x:
        x = y
        y = (x + n // x) // 2
    return x

def fermat(n):
    x = isqrt(n) + 1
    y = isqrt(x * x - n)
    while True:
        w = x * x - n - y * y
        if w == 0:
            break
        elif w > 0:
            y += 1
        else:
            x += 1
    return x - y, x + y

with open('publickey2.pem', 'r') as f:
    pub_data2 = f.read()

pubkey2 = RSA.importKey(pub_data2)
N2 = pubkey2.n
e = pubkey2.e

r, s = fermat(N2)

with open('ciphertext2.txt', 'r') as f:
    enc_f2 = int(f.read())

phi2 = (r - 1) * (s - 1)
d2 = inverse(e, phi2)
f1 = pow(enc_f2, d2, N2)

flag1 = long_to_bytes(f1)

x = r - f1 - 1

with open('publickey1.pem', 'r') as f:
    pub_data1 = f.read()

pubkey1 = RSA.importKey(pub_data1)
N1 = pubkey1.n
e = pubkey1.e

p = Int('p')
while True:
    np = gmpy.next_prime(x + f1)
    if np != r:
        break
    s = Solver()
    s.add(p**2 + x*p == N1, p > 0)
    if s.check() == sat:
        p = s.model()[p].as_long()
        q = p + x
        break
    x -= 1

with open('ciphertext1.txt', 'r') as f:
    enc_f1 = int(f.read())

phi1 = (p - 1) * (q - 1)
d1 = inverse(e, phi1)
f2 = pow(enc_f1, d1, N1)

flag2 = long_to_bytes(f2)

flag = flag1 + flag2
print flag
flag{F3rm@t_&_s0me_t1nk3r1ng_can_d0_w0nd3rs!!!!}

FuzzY (Forensics 200)

httpでフィルタリングし、success.txtを取得すると、PGP MESSAGEが含まれていた。

-----BEGIN PGP MESSAGE-----
Version: OpenPGP v2.0.8
Comment: https://sela.io/pgp/

wcBMA8fXP+32fyviAQf/T+NzsOgQ+ejW16GeK6h9WS9IDelAN9GLY5x5o9ilBlEL
G4IPati4/zqd+kyV5mmA7k2eKnNByRnxElpp0PoGULX0ykjBTcXuLtNXzGWcDsFF
xAkH8PduoPCcnNGWrCU6D8ZuWNtp7oeZ1krUZP+Kg9sfjjKfx0aUFhWs9SQH6mif
AlbJQwxKi2xXv0UsHvg4Mz4TpVstoO5XcN9d4V+gygc+wx0K61JwAFw96xptNi9y
hdMz/c7yW56JwBfwyiHvYmgLdWYJW9OEoQIj7Rwh1v8mD846vbvEDmagQ0Ra/K6q
lnxa37gBFE+4kYpSXP7yqr8QMhmGDpMROJoJqxYyY9JxAe6317HZ+UUEOmNR+0tB
EmPl/VVaoPc5q6RQ/cxwY4VhR4DtPsG9Gw237Sx+xSTAG5JbmtBf4KfQdVbeaXn1
PYPYBeCVL6nb6uPz6ZHBJ2SODWg9+Ssas+Gd5P7Q0zSA/35qYdamnAqUM/ujM2nN
k2U=
=+x+V
-----END PGP MESSAGE-----

これをsuccess.gpgとして保存する。秘密鍵もどこかにあるはずなので、探してみる。DNSの通信の中にPNGが含まれていたが、分散していたので、スクリプトで抽出する。

from scapy.all import *

packets = rdpcap('final_fuzz.pcap')

png = ''
for p in packets:
    if p.haslayer(IP) and p.haslayer(UDP):
        if p[IP].src == '192.168.42.129' and p[UDP].sport == 53:
            pad = p[Padding].load.lstrip('\x00')
            png += pad

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

この結果、QRコードの画像が抽出できる。
f:id:satou-y:20181028202214p:plain
このQRコードを読み取ると、以下の秘密鍵が取得できる。

-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: BCPG C# v1.6.1.0

lQOsBFvO/9wBCACgT4fK4dJm+M14jotXPUeKueo8xfFDunNUx/ZaSQbp5Y0i64OZ
dPkQk4E2zCgXaYKNRhiIx2RUy27GBf7xjtDb0gh/HNhC41f5ZzYrNQBEcabcr0hn
VfwiEzAqmTg+5TNsG26ZD2kuO1/J5zbKxI1D3g/9//fe5Nw8GucDiOntKgvFEXeV
ETZ0llbP/mh8SAn5+naJiJJri9y3GF+QhX7wYP+W6mBkano8X/Yk2B2qWIRT6wRU
DMQy1ptavyv5EJhYbsQGeAMu7WPJN+mLutAE2E1Xj03Sevsx2ynN8b/jF/HYp/mZ
SzZ+TbHlRUoMC4+hYh5XfXy7Cx9HSI0uIDShABEBAAH/AwMCEv0ZoXbeXxxg3ioH
/Y0lUhYOormsNzbrBjl1ipyWTDmRAf9BhmAPrX9K5GPAFAurGOj8QOQEWGrOyXfk
gYtHXzGk1K6ItCitgxdBqHgbti23Ht8SmVWw3/pijPXXerXXMqj6NQ95ma6bYPsU
PRtE1qtiEs+T8ln6ZBU9BCNyuZDceBY6btZS0cp88wB1xEPorhXVtjiV1cjDRSFG
licqXh4fr4Qe0TUEeZK1uTqlhdj6YvKoFP94OKGxeM0eR1R/H/zyOtJVMMsEZLGr
GNSVBZBN0B6l9wAMa+DGpuHIX25I197vP3x0v0gvP/57bF9og9mj2JzntM9NJsR1
2zAgplgX4IUp4SGPvcNbLE5c9yIEj77SAOBumrF3IcUYNN9IXvIHQh8qzOWmI5q+
NFCKin0tQNCAx4ef+4ThkyPezRovlFxG6T4HMF1YjYrlVMgiN034opaCKoFXd3EF
4UufN3vV0IYB7AfxWLeNAJyPCreDSyYyLFGx+ONpM5JKk/1cwH8H0XQuLXY+TuGj
iF6QWkRkVcAYv0F7r2wPaVOVa8465s34fY94Rv1+KCpsjNFc3FrAJhz84jETxxqr
s44U/zmGh0/tixjs9vB1C/i7csYWXYJYiPsPmcp5sOE4M1PtYsIfuOlaJ12e/IV9
YnNK+RLghYQ0MghUMHZeg8aqKY7SATDB1SuK+YKmhXte/E/VhTBUy+3RautMIUwS
w9R1z2Hh4POZ4kp8yj9PnEujoQ5XtZuruhNyiWEwWYf1GPuDSoeEYcIRj18h8dL+
OSvsS1DqgPxH9hy8iidLDq1ZkrQ0w08Du9zjVY02f4OoRunzXbis6P3Y0mRf4iif
bYdqVg3snMwY5u9lEaIYqmtcGibgybah394CgTt0xrQTa2FydGlrOTk3QGdtYWls
LmNvbYkBHAQQAQIABgUCW87/3AAKCRDH1z/t9n8r4qnlB/9N1BBWSf6lfmejPh3R
DZ+QrBsCELm8qBeawlsY9To6UUdrIoC9vzIwKAgil2K2MC9z/laZQcep0WepnOar
5KSUyhPI50/aE97yfA0v4lKkylb0OPt8E0S4gIxTlRhpht2K4lsRaD+2wyRvMRuU
/Grgxd5TVVm9KfXQBCAxgFgX2OdZ2/Yb2GJQ4M6DquISIBar+i39a9bdZ9kP70ox
jfgG8SLXPxzBiHIULUy4X+80VafKWw1/AzN2t4CTRtIMHu7jeUqpws+MB6TxTLBA
G/JSdb+W3ceHseJ9YXqVIhfrlKt8T3QAqErjQjPN0YB9KDaELwDM1rxFryy8zuAB
zdZ/
=rb5z
-----END PGP PRIVATE KEY BLOCK-----

これをprivate.ascとして保存する。
復号してみる。

$ gpg --import private.asc
gpg: 鍵F67F2BE2: 秘密鍵をインポートしました
gpg: /home/ctf/.gnupg/trustdb.gpg: 信用データベースができました
gpg: 鍵F67F2BE2: 公開鍵"kartik997@gmail.com"をインポートしました
gpg: 処理数の合計: 1
gpg:               インポート: 1  (RSA: 1)
gpg:       秘密鍵の読み込み: 1
gpg:   秘密鍵のインポート: 1
$ gpg success.gpg

次のユーザの秘密鍵のロックを解除するには
パスフレーズがいります

パスフレーズが必要なので、探してみる。先ほどのQRコードPNGファイルのzTXtチャンクに怪しいデータがある。

4578696600004d4d002a000000080003010e00020000000b000000320128000300000001
000200000213000300000001000100000000000068656c6c6f776f726c640000

末尾のデータをhexデコードする。

$ echo 68656c6c6f776f726c64 | xxd -r -p
helloworld

これをパスフレーズにして復号する。

$ gpg success.gpg

次のユーザの秘密鍵のロックを解除するには
パスフレーズがいります:"kartik997@gmail.com"
2048ビットRSA鍵, ID F67F2BE2作成日付は2018-10-23

gpg: *警告*: 暗号アルゴリズムAES256は受取人の優先指定に入っていません
gpg: 2048-ビットRSA鍵, ID F67F2BE2, 日付2018-10-23に暗号化されました
      "kartik997@gmail.com"
$ cat success
flag{eNcryP7!ng_t0_PgP_1s_r34LLy_Pre3tY_g00D_pr1V4cY}
flag{eNcryP7!ng_t0_PgP_1s_r34LLy_Pre3tY_g00D_pr1V4cY}