ふるつき

v(*'='*)v かに

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:この形式に名前ついてないんですか