Daily AlpacaHack 2026/2/20 の author’s write-up です。
導入
改行文字 \n を出力するとカーソルが下の行に動くのは有名でしょう。
状況によっては、\n 単体だと行頭ではなく真下に動きます(stty -onlcr や stty onlcr などで切り替えられます)。そういう場合でも \r を用いると行頭まで戻ってくれます。\f や \v で真下に動いてくれたりもします。
また、少し非有名かもしれませんが \b を出力すると左に動いてくれます。
文字を上書きせずに右に移動させるのは...? 少なくとも、(出力した文字列を覚えておけば)同じ文字を書くことで見かけ上そうすることは可能そうです。
では、上に動かすことは可能でしょうか?
このような問いは、CLI 用の少し凝ったプログラムを書こうとしたときに生じがちです。 複数行に渡るプログレスバーを(自前で)実装しようとしたりすると、たぶんそういう気持ちになると思います。
今回の問題はそうした機能に関連するものです。
解法例
まず、print-flag.sh を見ていきます。単に実行すると次のような出力を見ることになるはずです。
% から始まる行で入力、#> から始まる行で出力を表すことにします。出力が長い場合は #: で省略を表します。
% bash print-flag.sh #> Here is the flag: #> ... but it has been wiped away.
しかし、単にこの文字列にデコードされるような Base64 文字列は次のようになります。
SGVyZSBpcyB0aGUgZmxhZzoKLi4uIGJ1dCBpdCBoYXMgYmVlbiB3aXBlZCBhd2F5Lgo=
print-flag.sh 内にある文字列はもっと長いですから、なにか制御文字を用いた細工があるのでしょう。ということで、xxd(1) や hexdump(1) などを用いて見てみましょう。
% bash print-flag.sh | xxd #> 00000000: 4865 7265 2069 7320 7468 6520 666c 6167 Here is the flag #> 00000010: 3a0a 1b5b 3f31 3034 3968 1b5b 3142 1b5b :..[?1049h.[1B.[ #> 00000020: 313b 3338 3b35 3b31 3936 6d41 1b5b 6d1b 1;38;5;196mA.[m. #: #> 000003a0: 396c 2e2e 2e20 6275 7420 6974 2068 6173 9l... but it has #> 000003b0: 2062 6565 6e20 7769 7065 6420 6177 6179 been wiped away #> 000003c0: 2e0a ..
先ほど見えていなかった \x1b[?1049h や \x1b[1B などの文字列がたくさん見えます。
たとえば consols_codes(4) の ECMA-48 CSI sequences の章を見ると下記のことがわかります。
ESC[nA- n 行だけ上に移動する。
ESC[nB- n 行だけ下に移動する。
ESC[nC- n 列だけ右に移動する。
ESC[nD- n 列だけ左に移動する。
ESC[K- カーソルより右側を削除する。
また、ECMA-48 Select Graphic Rendition の章を見ると次のことがわかります。
ESC[1m- 太文字にする。
ESC[38;5;xm- 文字色を x に対応する色に変える。
ESC[m- 文字色(など)を元に戻す。
ESC [ ? 1049 h と ESC [ ? 1049 l については載っていないですが、DEC Private Mode (DECSET/DECRST) sequences を見るに何らかのスイッチを切り替えているものだと推測できます。GNU Screen のマニュアルの 11.1 Control Sequences を見ると Alternate Screen (new xterm code) という記述があります。echo $'\x1b[?1049h' や echo $'\x1b[?1049l' などを試すと画面が切り替わる様を見て取れると思います。
以上を踏まえると、結局は次のような処理をしているということがわかります。
- 画面を別の面に切り替える
- カーソルを上下左右に移動させたり文字色を変えたりしつつ、フラグを構成する文字を出力する
- 文字列を消す
- ダミーの文字列で上書きする
- 画面を元の面に戻す
ということで、文字列を消す処理の前で打ち切ることで、フラグを画面に残すことができます。
solve-1.sh
echo $'\x1bc' bash print-flag.sh | cat -vet | sed -E '2!d; s/\^\[\[\?1049[hl]//g; s/\^\[\[K.*//; s/\^\[/\x1b/g' echo echo
ここで echo $'\x1bc' (ESC c) は画面をリセットするのに対応していて、clear コマンドや C-l (Control+L) のキーバインドをするのと同等の効果です。
あるいは、各文字の出力間隔を数ミリ秒空けることで、実際に dancing cursor な状況を見ることもできます。適切なタイミングで C-c を押して中断させましょう。
solve-2.sh
bash print-flag.sh |
basenc --base16 |
fold -w2 |
while read -r hh; do
echo $hh | xxd -p -r; sleep 0.02
done
あるいは、カーソルの移動の箇所を自前で実装して再現させる解法ももちろんあります。
solve-3.py
from base64 import b64decode input_base64 = "SGVyZSBpcyB0aGUgZmxhZzoKG1s/MTA0OWgbWzFCG1sxOzM4OzU7MTk2bUEbW20bWzE7Mzg7NTsyMDhtbBtbbRtbMTszODs1OzIyNm1wG1ttG1sxOzM4OzU7MjAybWEbW20bWzE7Mzg7NTsyMjBtYxtbbRtbMTszODs1OzIxNG1hG1ttG1sxbXsbW20bWzQ2QxtbMW19G1ttG1sxQhtbMTFEG1szODs1OzE5MW0jG1ttG1sxQRtbN0RuG1sxM0RfG1syNkNsG1szNER1G1s1RHMbWzZEdBtbNkNTG1sxMEMwG1sxOERyG1sxQRtbOEQbWzM4OzU7MjI2bSsbW20bWzFCG1szQ28bWzQwQ3QbWzM0REUbWzNEXxtbMUIbWzJEG1szODs1OzIyN21vG1ttG1syQRtbMkMbWzM4OzU7MjI3bS4bW20bWzFCG1syM0NyG1szNERDG1szNENvG1sxNkRoG1sxQ3VsG1sxOUNyG1sxQhtbNTBEG1szODs1OzIyNm0qG1ttG1szNUMbWzM4OzU7MTkwbSsbW20bWzJBG1s0RBtbMzg7NTsyMjZtLhtbbRtbMTFDG1szODs1OzIyN21vG1ttG1sxQhtbMzREbBtbMUEbWzhDG1szODs1OzE5MW1vG1ttG1sxQhtbOUNfG1sxQhtbMTREG1szODs1OzIyMW0rG1ttG1syQRtbMTBEG1szODs1OzE5MG0jG1ttG1sxQhtbMzVDYxtbMTNEMxtbMUIbWzE0QxtbMzg7NTsyMjFtKhtbbRtbMUEbWzI5RG4bWzlDZBtbMUEbWzREG1szODs1OzIyMW0rG1ttG1syQhtbMjJEG1szODs1OzIyN20uG1ttG1sxQRtbMThDcxtbMUEbWzEzQxtbMzg7NTsxOTFtIxtbbRtbMUIbWzZDbxtbN0RfG1szMERvG1szMEN1G1syMERzG1s3REMbWzFCG1s2QxtbMzg7NTsyMjZtKhtbbRtbMUEbWzEzQzMbWzE3RGUbWzREZRtbMzBDbxtbNERuG1s0MERuG1szNUNfG1sxMER1G1syRF8bWzE0RGMbWzIxQ3IbWzEyRGIbWzFCG1szRBtbMzg7NTsxOTFtbxtbbRtbMUEbWzZDZBtbMUEbWzM5RBtbSxtbMkIbW0sbWzFBWGlxeG94ez09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT19ChtbPzEwNDlsLi4uIGJ1dCBpdCBoYXMgYmVlbiB3aXBlZCBhd2F5Lgo=" input_ = b64decode(input_base64).decode() res = [[" "] * 96 for _ in range(5)] buf, esc = [0], False i, j = 0, 0 for c in input_: if esc: if "0" <= c <= "9": buf[-1] *= 10 buf[-1] += int(c) elif c == "h" or c == "l": buf, esc = [0], False elif c == ";": buf.append(0) elif c == "A": i -= buf[-1] buf, esc = [0], False elif c == "B": i += buf[-1] buf, esc = [0], False elif c == "C": j += buf[-1] buf, esc = [0], False elif c == "D": j -= buf[-1] buf, esc = [0], False elif c == "?": continue elif c == "m": buf, esc = [0], False else: if c == "\x1b": esc = True elif c == "X": break elif c == "\n": i += 1 j = 0 else: res[i][j] = c j += 1 for x in res: print("".join(x))
以上によりフラグが得られます。👏
Here is the flag:
+ # . o + . # o
Alpaca{Control_sESCuences_sh0uld_b3_und3r_our_control}
* . o + * o + # *
フラグの文字列は ESC や \033 との掛詞でした。sequence と掛けたのは強引だったかもしれませんが、前回もそういう感じでしたし別によいでしょう。evanESCent という単語もあるらしいです。
あと、ダミーの文字列は適当な感じにしましたが、Llama{================} とか Redacted{================} とかにしておけばよかったかなと後から思いました。
あとがき
バイナリをうっかり cat してしまったときに、\a が発火して ピコッピコッ と鳴りながら文字化けした ??? が画面を流れていく様を見せられたり、その後何らかの理由でターミナルが不調になった経験は誰しもあることでしょう。えびちゃんもあります。
なにかが起きるときの状況を再現させようということで、下記のようにしてファイルを作って cat したりしていました。
head -c 128 /dev/random > random-$(date +%s.%N).dat
echo $'\x1b]\x1bZ' や echo $'\x1b[c' を実行するとターミナルに 1;2c が入力された状態になったりします。
echo $'\x1b[>c' だと 1;95;0c になりました。実際には ESC ?1;2c みたいなものが返ってきているらしいです?
echo $'\x1b#8' を実行すると、画面が E で埋め尽くされたりします。遊んでいて変な状態になったら echo $'\x1bc' を実行すると助かる可能性があります。
echo $'\x1b[?7l' を実行すると、カーソルが右端まで達しても勝手に改行してくれなくなります。こちらは echo $'\x1bc' では治ってくれず、echo $'\x1b[?7h' などをする必要がありそうです。他にも、echo $'\x1b[?25l' でカーソルが消えたりします。いろいろと実装されているものですね。
今までは「ターミナルがこわれたらとりあえず stty sane を試して、治らなかったら画面を開き直す選択肢しか残らなくなる」という感じでしたが、少し成長した気がします。
こちらなども読んでみるとよいかもしれません。ncurses(3x) とかを読むのも面白そうかもしれません。
おわり
おわりです。また出題した際にはよろしくお願いします。