ふるつき

v(*'='*)v かに

Square CTF Writeup

Square CTF Writeup

 2018/11/9 - 2018/11/15 の期間、insecure(ptr-yudai, yoshiking, theoldmoon0602, hikopiyo) として SquareCTF に挑戦していました。中盤、ptr-yudai が実力を遺憾なく発揮して 2-4位をうろつく展開だったのですが、最終問題を解くことができずに84291点で8位という結果でした。 First Blood からの時間経過で配点が下がることを知らずに(つまり早く解くほど点数が高い)最初の問題に出遅れたこと、最後の問題が解けなかったことが悔やまれますが、8位はなかなか高順位で、Top 10 teams にも入れたことは嬉しいです。

 解いた問題の内訳としては、 hikopiyoが1問、私が1問、残りの7問を ptr-yudai が解きました。私が解いた問題についてWriteupを書きます。

[Rev 86Solves] gofuscated

 名前からは Go の難読化したバイナリが渡されるのかなという感じですが、実際には以下のソースコードが渡されただけでした。

package main

import (
    "encoding/hex"
    "fmt"
    "math/rand"
    "os"
    "regexp"
    "strings"
    "time"
)

/**
 * important computation, part 1 of 2
 */
func compute1(done chan bool) {
    fmt.Printf("\n\n\n\n\n\n")
    frames := []string{
        "\033[6A\r               X \n                  \n               O  \n             Y/|\\Z\n               |\n              / \\\n",
        "\033[6A\r                 \n             X    \n             Y_O  \n               |\\Z\n               |\n              / \\\n",
        "\033[6A\r                 \n             XY   \n              (O  \n               |\\Z\n               |\n              / \\\n",
        "\033[6A\r              Y  \n                  \n             X_O  \n               |\\Z\n               |\n              / \\\n",
        "\033[6A\r               Y \n                  \n               O  \n             X/|\\Z\n               |\n              / \\\n",
        "\033[6A\r                 \n                 Y\n               O_Z\n             X/|  \n               |\n              / \\\n",
        "\033[6A\r                 \n                ZY\n               O) \n             X/|  \n               |\n              / \\\n",
        "\033[6A\r                Z\n                  \n               O_Y\n             X/|  \n               |\n              / \\\n",
    }
    ctr := 0
    for {
        select {
        case <-done:
            return
        case <-time.Tick(time.Duration(250) * time.Millisecond):
            ctr++
            s := frames[ctr%len(frames)]
            x := []byte("\033[32mo\033[39m")
            y := []byte("\033[34mo\033[39m")
            z := []byte("\033[35mo\033[39m")
            for t := 0; t < ctr/len(frames)%3; t++ {
                x = xor_slice(xor_slice(x, y), z)
                y = xor_slice(xor_slice(x, y), z)
                z = xor_slice(xor_slice(x, y), z)
                x = xor_slice(xor_slice(x, y), z)
            }
            s = strings.Replace(s, "X", string(x), 1)
            s = strings.Replace(s, "Y", string(y), 1)
            s = strings.Replace(s, "Z", string(z), 1)
            fmt.Print(s)
        }
    }
}

const Output = 16
const Space = 100000
const Rounds = 100000

/**
 * important computation, part 2 of 2
 *
 * pro-tip: you should always roll your own crypto. This prevents the NSA or other attackers from using
 * off-the-shelf tools to defeat your system.
 */
func compute2(data []byte, done chan bool) chan string {
    r := make(chan string)

    go func() {
        state := make([]int, Space)
        j := 0
        i := 0
        for i = range state {
            state[i] = i
        }

        for t := 0; t < Space*Rounds; t++ {
            i = (i + 1) % Space
            j = (j + state[i] + int(data[i%len(data)])) % Space
            state[i], state[j] = state[j], state[i]
        }

        o := make([]byte, Output)
        for t := 0; t < Output; t++ {
            i = (i + 1) % Space
            j = state[(state[i]+state[j])%Space]
            o[t] = byte(j & 0xff)
        }

        done <- true
        r <- hex.EncodeToString(o)
    }()

    return r
}

/**
 * Some other computation.
 */
func compute3(r chan byte) chan byte {
    s := make(chan byte)
    go func() {
        var prev byte = 0
        for v := range r {
            if v != prev {
                s <- v
                prev = v
            }
        }
        close(s)
    }()
    return s
}

/**
 * These aren't helpful, right?
 */
func compute4(input string) string {
    rand.Seed(42)
    m := make(map[int]int)
    for len(m) < 26 {
        c := rand.Int()%26 + 'a'
        if _, ok := m[c]; !ok {
            m[c] = len(m) + 'a'
        }
    }
    r := ""
    for _, c := range input {
        r = fmt.Sprintf("%s%c", r, m[int(c)])
    }
    return r
}

/**
 * A boring helper function
 */
func xor_slice(a []byte, b []byte) []byte {
    r := make([]byte, len(a))
    for i, v := range a {
        r[i] = v ^ b[i]
    }
    return r
}

/**
 * Another boring function
 */
func panicIfInvalid(s string) {
    if !regexp.MustCompile("^[a-zA-Z0-9]{26}$").MatchString(s) {
        panic("invalid input!")
    }
}

/**
 * Last one
 */
func another_helper(input string) (r bool) {
    r = true
    for i, v := range input {
        for j, w := range input {
            r = r && (i > j || v <= w)
        }
    }
    return
}

/**
 * Pro-tip: start here.
 */
func main() {
    input := os.Args[1]
    panicIfInvalid(input)

    done := make(chan bool)
    go compute1(done)
    h := compute2([]byte(input), done)

    s := make(chan byte, len(input))
    r := compute3(s)
    for _, c := range input {
        s <- byte(c)
    }
    close(s)

    input = ""
    for c := range r {
        input = fmt.Sprintf("%s%c", input, c)
    }
    panicIfInvalid(input)

    input = compute4(input)
    flag := <-h
    if !another_helper(input) {
        panic("invalid input!")
    }
    panicIfInvalid(input)

    fmt.Printf("Congrats! 🚩  = flag-%s\n", flag)
}

// author: Alok

 みるとわかりやすいコメントが書かれています。読み進めていくと以下のことがわかりました。

  • panicIfInvalid は入力が ^[a-zA-Z0-9]{26}$ であることを期待している
  • compute1 はお手玉を表示するだけで特に読まなくて良い
  • compute2 は入力を受け取ってswapなどをしてあと、計算結果が flag := <-h と用いられており、本体っぽい。
  • compute3 は入力を受け取ってuniq をして return している。そのあと panicIfInvalid に渡されていることから、入力に同じ文字の連続がないことがわかる
  • compute4 は compute3 された入力を受け取って独自のマッピングで置換している。
  • another_helper は 入力文字C_i について i < j なら C_i < C_j であることを保証してくれている(演算子<= だけど compute3 の制約で = になることはない)。文字数が26文字であることから考えて、 compute4 の出力は abcdefghijklmnopqrstuvwxyz になる。

 compute3, 4と another_helper のおかげで入力が一意に定まりそうです。 compute4 内で作成していた置換テーブルを逆にしたものを abcdef...xyz に適用してやると、nxelvzqaifsyhojudrbcwgptmk と出てきたので、これを入力してやれば compute2 ががんばってFlagを出力してくれそうです。

 ……というところまで解析してSlackに投げて走らせたら、ptr-yudaiのほうが先にflagが出力されてSubmitしてくれました。実質私の得点じゃないじゃん。私のマシンだと全然フラグでてこなかった。

そのほか感想など

  • 解析そんなに難しくないしもっと早く解けるべきだった。結構compute1, compute2を読む方を頑張ってしまっている時間が長くて失敗。
  • すわ1位か!? というところまで頑張ったのに最後の問題解けなくてがっかり。惜しいところまで解析できていたのですが、惜しいだけじゃあ点数にはならないんだよな。
  • ptr-yudaiが各問題を爆速で解いていたのですごかった。ptr-yudaiがGo読めたら僕の出番がなくなるところでした。

 おわり。SquareCTFは早くも来年の予定を公開しているので、またinsecureでチャレンジして、今度は勝利を掴みたいところです。

HCTF 2018 Writeup

HCTF 2018 Writeup

2018/11/9 21:00 - 11/11 21:00 の 48時間、 insecure (ptr-yudai, yoshiking, theoldmoon0602) として、 HCTF に参加していました。日付と時間帯が大変都合がよく、食事睡眠お風呂炊事以外の時間はずっとCTFに取り組み、およそここまでの人生で最もCTFに染まった2日間になりました。

私は3問を解き、yoshikingが1問、残りをptr-yudaiが解いてinsecureは3032.49点を獲得。得点 292チーム中 22位でした。輝かしい功績ですが、突っ込んだリソースから言えばもう少し稼ぎたかったところです。ちなみにWebとPwnはまるでわからなくて、MiscとCrypto、少しのBin+BlockChainで稼ぎました。

このCTFは解いたチーム数に応じて問題の得点が変化するタイプ*1で、小数点までついてます。

[Misc 424.63] Easy Dump

Miscの皮をかぶったForensic問題です。mem.data という名前の256MBほどのバイナリが与えられます。 strings などをした結果、どうやらWindowsのファイルらしいぞということがわかりました。まずディスクダンプを疑ったのでSleuth Kitにかけてみたのですが反応がなく、続いてメモリダンプを疑ってVolatilityを試すとこちらが当たりでした。

volatility -f mem.data pstree としたところ、以下のようになりました。

Name                                                  Pid   PPid   Thds   Hnds Time
-------------------------------------------------- ------ ------ ------ ------ ----
 0xfffffa80003a5040:System                              4      0    108    373 2018-11-07 08:12:31 UTC+0000
(略)
.. 0xfffffa8002c62060:vmtoolsd.exe                   1356    472      9    225 2018-11-07 08:12:37 UTC+0000
... 0xfffffa8000cfeb30:cmd.exe                       2824   1356      0 ------ 2018-11-07 08:26:51 UTC+0000
(略)
 0xfffffa8001d64b30:explorer.exe                     1696   1548     20    661 2018-11-07 08:12:38 UTC+0000
. 0xfffffa8003344390:wordpad.exe                     1804   1696      3    120 2018-11-07 08:15:35 UTC+0000
. 0xfffffa8000d9ab30:MineSweeper.ex                   312   1696      9    208 2018-11-07 08:15:39 UTC+0000
. 0xfffffa8002de1560:mspaint.exe                     2768   1696      6    122 2018-11-07 08:16:05 UTC+0000
. 0xfffffa8002d0cb30:vmtoolsd.exe                    2028   1696      9    199 2018-11-07 08:12:39 UTC+0000
 0xfffffa8001fe1060:csrss.exe                         424    408     11    197 2018-11-07 08:12:33 UTC+0000
 0xfffffa8002638670:winlogon.exe                      504    408      3    114 2018-11-07 08:12:33 UTC+0000

cmd.exe を疑って cmdscan をしたのですが何も得られず、続いて mspaint.exe にあたりをつけました。volatility -f mem.data --profile=Win7SP1x64 memdump -p 2768 --dump-dir=dump としてメモリダンプを抽出し、それをgimpで開くと一部文字のようなものが見える部分がありました。頑張って幅を調整して、画像を反転させるとフラグが書いてありました。

f:id:Furutsuki:20181111220012p:plain

 問題名のとおり簡単でしたが、ここまでたどり着くのに時間を大きく奪われてしまったので反省です。

[Misc 432.97] Difficult Programming Language

 pcapファイルが渡されます。中身をみるとUSBのパケットで、キーボードっぽいなと思いました。むしろUSBのパケットを渡されてキーボード以外だったことがまだない。

 とりあえず tshark -Tfields -e usb.capdata -r difficult_programming_language.pcap > data のようなことをしてデータ部を取り出します。tsharkの使い方は私のブログの過去記事に助けられました。dataは以下のようになります。

02:00:00:00:00:00:00:00
02:00:07:00:00:00:00:00
02:00:00:00:00:00:00:00
00:00:00:00:00:00:00:00
00:00:34:00:00:00:00:00
00:00:00:00:00:00:00:00
00:00:35:00:00:00:00:00
00:00:00:00:00:00:00:00
...

 さて、このデータから何が入力されていたのかを辿りたいのですが、真面目にキーストローク文字コードを変換するのは大変なので、インターネットの力↓に頼りました。ありがとうございます。

yocchin.hatenablog.com

 データのフォーマットを反映して少しスクリプトを変更しました。

keymap = {  0x04: ('a','A'), 0x05: ('b','B'),0x06: ('c','C'),
        0x07: ('d','D'), 0x08: ('e','E'),0x09: ('f','F'),
        0x0a: ('g','G'), 0x0b: ('h','H'),0x0c: ('i','I'),
        0x0d: ('j','J'), 0x0e: ('k','K'),0x0f: ('l','L'),
        0x10: ('m','M'), 0x11: ('n','N'),0x12: ('o','O'),
        0x13: ('p','P'), 0x14: ('q','Q'),0x15: ('r','R'),
        0x16: ('s','S'), 0x17: ('t','T'),0x18: ('u','U'),
        0x19: ('v','V'), 0x1a: ('w','W'),0x1b: ('x','X'),
        0x1c: ('y','Y'), 0x1d: ('z','Z'),0x1e: ('1','!'),
        0x1f: ('2','@'), 0x20: ('3','#'),0x21: ('4','$'),
        0x22: ('5','%'), 0x23: ('6','^'),0x24: ('7','&'),
        0x25: ('8','*'), 0x26: ('9','('),0x27: ('0',')'),
        0x28: (' [Enter] ',' [Enter] '), 0x29: ('\x1b','\x1b'),
        0x2a: (' [del] ',' [del] '), 0x2b: ('\x09','\x09'),
        0x2c: ('\x20','\x20'), 0x2d: ('-','_'),
        0x2e: ('=','+'), 0x2f: ('[','{'),0x30: (']','}'),
        0x31: ('\\','|'), 0x33: (';',':'),0x34: ('\'','\"'),
        0x35: ('`','~'), 0x36: (',','<'),0x37: ('.','>'),
        0x38: ('/','?'),
        0x51:(' [downArrow] ',' [downArrow] '), 0x52: (' [upArrow] ',' [upArrow] '),0x32: ('\\','|')
        }

def analyze_usb_data(usb_data):
    flag = ""
    for d in usb_data:
        if d[2] == 0 or not(0 in d[3:8]):
            #No Event
            continue
        if d[0] == 0 or d[0] == 0x20:
            #press shift
            #binary -> int
            c = keymap[d[2]][0]
            flag += c
        else:
            #binary -> int
            c = keymap[d[2]][1]
            flag += c
    print flag

def main():
    data = list(map(lambda x: list(map(lambda y: int(y, 16), x.split(":"))), open("data").read().splitlines()))
    analyze_usb_data(data)

if __name__ == '__main__':
    main()

 ちなみに何かを途中で間違えたらしく、シフトキーを押したかどうかの判定が逆になっており、途中で時間をとられました。正しく変換してやると、次のような文字列になります。

D'`;M?!\mZ4j8hgSvt2bN);^]+7jiE3Ve0A@Q=|;)sxwYXtsl2pongOe+LKa'e^]\a`_X|V[Tx;:VONSRQJn1MFKJCBfFE>&<`@9!=<5Y9y7654-,P0/o-,%I)ih&%$#z@xw|{ts9wvXWm3~C

 これであっているのかと不安になる記号列ですが、問題名に従ってMalblogeというプログラミング言語ソースコードであるとあたりをつけました。インタプリタがないか探したところ以下がヒットしたので実行し、フラグを得られました。

https://zb3.me/malbolge-tools/

[Crypto 424.63] xor?rsa

 次のようなソースコードと、サーバの動作情報が与えられます。 verify なる関数が謎過ぎて悩んでいましたが、ptr-yudai が「チームトークンだよ」と教えてくれました(このCTFではチーム毎に、「チームトークン」なる文字列が与えられ、時々問題の最初に入力を求められました)。

from Crypto.Util.number import *
import SocketServer
import string
import hashlib
import random
import requests
import json
from flag import *

class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass


class RSATCPHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        self.request.sendall("Welcome to flag getting system\ngive me your token > ")
        token = self.request.recv(1024).strip()
        if not verify(token):
            self.request.sendall("token error\n")
        else:
            p = getStrongPrime(1024)
            q = getStrongPrime(1024)
            n = p * q
            e = 5
            nbits = size(n)
            kbits = nbits // (2 * e * e)
            m1 = getRandomNBitInteger(nbits)
            m2 = m1 ^ getRandomNBitInteger(kbits)
            c1 = pow(m1, e, n)
            c2 = pow(m2, e, n)

            self.request.sendall("n=" + str(n) + "\n")
            self.request.sendall("c1=" + str(c1) + "\n")
            self.request.sendall("c2=" + str(c2) + "\n")

            self.request.sendall("now give me you answer\n")
            ans1 = self.request.recv(2048).strip()
            ans2 = self.request.recv(2048).strip()

            if str(ans1) == str(m1) and str(ans2) == str(m2):
                self.request.sendall(FLAG)
            else:
                self.request.sendall("wrong answer\n")

if __name__ == "__main__":
    HOST, PORT = "0.0.0.0", 10086
    server = ThreadedTCPServer((HOST, PORT), RSATCPHandler)
    server.serve_forever()

 ソースコードをみるとわかるように、 m2は小さめのデータを生成したあと、m1とのxorをとっています。はじめはeが小さかったのでLow Public Exponent Attackかなと思っていましたがはずれで、m1とm2の上位ビットが共通することからFranklin-Reiter Related Message Attackをやることにしました。

 Franklin-Reiter Related Message Attackではこちら↓の記事が、(多分同じ出典ということでしょうけど)そっくりのスクリプトを使っていたので、同じコードで攻撃できると判断してソースコードを拝借しました。

 

inaz2.hatenablog.com

import sys

def short_pad_attack(c1, c2, e, n):
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))

    g1 = x^e - c1
    g2 = (x+y)^e - c2

    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)

    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()

    kbits = n.nbits()//(2*e*e)
    diff = h.small_roots(X=2^kbits, beta=0.5)[0]  # find root < 2^kbits with factor >= n^0.5

    return diff

def related_message_attack(c1, c2, diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]


if __name__ == '__main__':
    n = sage_eval(sys.argv[1])
    e = 5

    c1 = sage_eval(sys.argv[2])
    c2 = sage_eval(sys.argv[3])

    diff = short_pad_attack(c1, c2, e, n)
    m1 = related_message_attack(c1, c2, diff, e, n)
    print m1
    print m1 + diff

 接続用のスクリプトを組んで、フラグがでました。 str(int(res[0])) と一見無駄っぽいことをしていますが、これを挟まないと wrong answer が出ます。ちょっと謎挙動で時間を取られた。この問題はSageMathのインストールとこれが時間泥棒でした。

import sys
import subprocess
from pwn import *

def main():
    p = remote('rsa.2018.hctf.io', 10086)

    p.recvuntil('> ')
    p.sendline('M3SEdoxFHNsGWj5Gc6pN1vKqbP5jfKmn')

    n = p.recvline().split("=")[1]
    c1 = p.recvline().split("=")[1]
    c2 = p.recvline().split("=")[1]

    res = subprocess.check_output(["/home/user/Downloads/SageMath/sage", "attack.sage", n, c1, c2]).splitlines()

    print(p.recvuntil('answer\n'))

    p.sendline(str(int(res[0])))
    p.sendline(str(int(res[1])))

    p.interactive()

main()

感想など

  • 1282.23 / 3032.49 ということなのでいい具合かもしれない。しかし私が解いた問題はSolve数が多いものばかりで差をつけることができてない。もっと難しい問題も解けるようになって得点源になりたい。
  • Lucky Star☆は解きたかった。途中まで解析して諦めてptr-yudaiに投げたら解いていました。絶対Writeupに「おっはラッキー☆」って書きたかったのに。
  • 同様にSpiralは「天元突破グレンラガン」だったので解きたかった。このRev激ムズでは、解いたチーム本当に解析したんですか。どうやって。Writeup読もう
  • xor_gameはptr-yudaiが解いてたけど最初の方時間を溶かして全ての方針を折られたので何をやればよかったのか気になる。
  • adminはなんで解けなかったのかわからない。ソースコードを見るところまでは行けて(なんでコメントにGitHubのURLが書かれることになるんだろう)、SECRET_KEYがそのままだからセッション書き換えればいいのねって書き換えても弾かれ、それがだめならremember_tokenと思ってこちらをやっても反応なし。なんでだろう。この問題ちゃんと実装されていない箇所もあって嫌い
  • PolitishDuckも解けなかった。つら
  • その他のWeb問は全部エスパーでしょ。なんもわからん。特にshareはヒント0で解ける人絶対0人だって
  • yoshikingがBlockChain問題解いててすごい。今度教えてもらいます。
  • このCTFのPlatform、セッションがすぐ切れてログインを求められるんだけどそのたびにyoshikingのメールアドレスを打ち込むのがかなり負担。つらい。
  • 22位はちょっとすごいと思ってる。ptr-yudaiはそのうち去ってしまうのでそれまでに力をつけて、ptr-yudaiなしでもこのくらい頑張れるようになりたい。

*1:この形式に名前ついてないんですか

SECCON 2018 ONLINE Ghost Kingdom Writeup

 insecure という解散間近のチームで SECCON ONLINE に参加していました。移動と食事、睡眠以外の時間はやっていたので最長記録かも。 insecure は 国内決勝の進出を決めているので気合が入っていました。

 結果はよく知らないですが、私が Ghost Kingdom を解いたのと、残りを師匠が解いたので、私はGhost Kingdom のwrite upをします。


  • ログイン・レジスタができます
  • ログイン画面には(from internet) とあります
  • ログインすると「Adminにメッセージ」「すきなページのスクリーンショットをとる」機能があります
  • 「画像をアップロード」はローカルからのログインしかできません

 師匠が スクリーンショットのところで「locahost」と打って弾かれているのをみたので「0.0.0.0は?」と言って試してもらって、スクリーンショット経由でlocalのログイン画面にたどり着くことに成功しました。

 ログイン以外の操作ができずに悩んでいたのですが、スクリーンショットをとったり、Adminにメッセージを送るのはログイン画面を経由せずともできたので解決しました(ログインが不要だったのか、スクリーンショットをとってるブラウザがログイン済みだったのかはわからなかった)。admin にメッセージを送る際に、 input[type=hidden] にCSRFトークンとしてCGISESSID が使われていること、 get パラメータで css=hogehogeと、style タグの中身を base64encode したものを渡せるということに気が付いたので、 input[value^=0] { background: url("xssme?c=0") }input[value^=1] { background: url("xssme?c=1") } というふうにCSS injection を行いました。流石にこれを生成する スクリプトは書いた。こうしてスクリーンショットを撮っているユーザの セッションを盗み取ることができたので(手元のメモには 4341c2fdb42f0e839ba08c とあります)、このSESSIONを装備してログインしました。

 画像アップロード機能が解禁され、「JPGのアップロード(拡張子しかみてない)」「画像のGIFへの変換」ができるようになりました。明らかに ImageMagick をいじめる問題だったので、頑張ってGoogleして、下のようなPostScriptを aiueo.jpg としてアップロード、変換してフラグを得ました。問題の最初にフラグの場所が書いてあったのにそれを忘れて find / -name "*flag*" とかで探し回っていたのは内緒です。

%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%cat /var/www/html/FLAG/FLAGflagF1A8.txt) currentdevice putdeviceprops

 特殊アーキテクチャ系はエミュレートがうまくできなかったので目で追いかけて python でシミュレートしたのですがうまくいきませんでした。なぜ。 classic pwn は解けなかった。 shooter は最終盤に SQLi を頑張ったけどテーブル名を抜くのが間に合わなかった。あれスクリプト書けるの。

あとで追記:

 SpecialInstructionsは xorshift32 の << 15<< 5 としていたため解けなかった模様。こんなんばっかでは