記号と英字2文字だけでbash

難読化シェル芸学会のみなさんこんにちは。
今回は数字を使わず、記号と英字2文字だけで任意のコマンドを実行する方法を紹介します。

(追記)完全記号化が実現されました

なお、本発表のコマンドはDockerのdebian:stretch-20190204(9.7)上のbash 4.4.12で検証しています。
また、OS X Sierra(10.12.60)上のbash 5.0.2でも検証しています。
Bash 4.x未満は対象外です。

本研究の目的

BashはBourne系を代表するシェルであり、多くの環境で標準的に採用されています。
そして、難読化シェル芸は入力された見た目と一見反するようなコマンドによってシェルを操作する芸当です。

$(printf "%b" $(printf '%s%x' '\x' $((0x83 ^ 0xe7))))$(ls --help|grep ^G|cut -c53)$(ls --help|grep ^G|cut -c10)$(ls --help|grep ^G|cut -c8)

上記コマンドを実行すると、現在の時刻が表示されます。
具体的には、$()で記述された各コマンド代入(command substitution)によってdateの文字がそれぞれ得られ、そのまま実行することでdateコマンドと等価な挙動を示しています。

この難読化シェル芸をより複雑に見せる手法として、記号と最小数の英数字を用いる手法(原則的に記号が用いられることから記号難読化シェル芸と呼ばれる)が複数提案されていますが、今回は数字を用いずに、そしてできるだけ環境に依存しない状態で記号難読化シェル芸を実現することを目的としました。

先行研究

記号と1, 2, A, zのみで難読化シェル芸

kanataさんによって発表された記号難読化シェル芸の手法です[1]

A=$(. 2>&1);A=${A##*.};${A:$((++z*++z*++z*z-z+++z)):$((z=z^z||++z))}${A:$((++z*++z---z)):$((z=z^z||++z))}${A:$((++z*++z*z)):$((z=z^z||++z))} -- {z..A};${@:$((++z*++z*++z---z+++z---z+++z---z)):$((z=z^z||++z))}${@:$((++z*++z*++z---z+++z---z+++z)):$((z=z^z||++z))}${@:$((++z+++z+--z)):$((z=z^z||++z))}${@:$((++z*++z*++z---z+++z---z)):$((z=z^z||++z))}

詳細は文献ブログを参照していただきたいのですが、簡単に述べます。
.コマンドによって得られるエラーメッセージを標準出力にリダイレクトし、変数$Aに格納します。

$ A=$(. 2>&1)
$ echo $A
-bash: .: ファイル名が引数として必要です
.: 使用法: . filename [arguments]

このうち、filename [arguments]だけは言語環境に依存しないので、その部分だけ抽出します。

$ A=${A##*.}
$ echo $A
filename [arguments]

つづいて、{A..z}でアルファベットを一気に獲得します。しかし、例えば1文字目を抽出するために{A..z}:1:1のように直接参照することはできないため、一度コマンドの引数とする必要があります。
ここでsetコマンドを使用すると、引数が位置パラメータ($@)に格納されるようです。

# set -- {z..A}
$ ${A:19:1}${A:4:1}${A:18:1} -- {z..A}
$ echo $@
z y x w v u t s r q p o n m l k j i h g f e d c b a ` _ ^ ] [ Z Y X W V U T S R Q P O N M L K J I H G F E D C B A

この位置パラメータから任意の文字を抽出して並べれば、任意のコマンドが実行できます。
数値の扱いについては割愛させていただきます。

# date
$ ${@:23:1}${@:26:1}${@:7:1}${@:22:1}
Tue Feb 19 17:10:48 UTC 2019

記号と1, 2のみで難読化シェル芸

たいちょーさんによって発表された記号難読化シェル芸の手法です[2]

____=($(__="$(. 2>&1)"; __=${__##*.}; ___=($(${__:2*2+2+1:1}${__:2*((1+2)*(1+2)):1} 2>&1)); ${___[2*2*2-1]:2+2:1}${___[1-1]:1:1} ${___[2*2*2-1]:1- 1:2*2+2}|${___[1-1]:1+2:1}${___[2+2+1]:1:1}${___[1-1]:2+2:1}${___[1+2]:1:1} -${___[1+2]:1-1:1} .|${___[1- 1]:1:1}${___[2*2*2]:1-1:2}${___[1]:1:1} ${___[11- 1]:1:2}|${___[1]:1:1}${___[1-1]:2:1}${___[12+1]:1- 1:1}${___[2*2*2-1]:2+2:1} - $((22*2+12+2))));${____[11+2+2]}${____[2*2*2+1]}${____[22*2+1]}${____[12+2*2+1]}

こちらについても簡単に解説します。
この手法で重要なのは、$_は直前のコマンドの引数を示す特別な変数であるのに対し、$__$___など、アンダースコアを複数並べた変数は予約されず、自由に使用できるという点です。

$ __="$(. 2>&1)"
$ __=${__##*.}
$ echo $__
filename [arguments]

また、ls --helpによって大量に文字を獲得することが可能です
(手元でも検証しましたが、[2]ではWSLを検証環境に使用されておりこちらと出力が異なったため、以下は原典の内容を表示しています)。

$ ls --help|grep -o .|sort -u|tail -58|tr -d '¥n'
012345678aAbBcCdDeEfFgGhHiIkKlLmMnNoOpPqQrRsStTuUvwWxXyYzZ

しかし、filename [arguments]からls --helpを実行するためには文字が足りません。
ここで、すでにある文字から入力できる、mtコマンドのヘルプメッセージから文字を取得されています(mtコマンドはWSLに用意されているようです、以下も原典より)。

$ mt
Usage: mt [OPTION...] operation [count]
Try `mt --help' or `mt --usage' for more information. 

したがって、手順としては

  • .コマンドのエラーメッセージからmtコマンドを打つための文字を取得
  • mtコマンドからls --helpを打つための文字を取得
  • ls --helpから大半の文字を取得→この結果を$____に配列として格納
  • $____の任意のインデックスから文字を参照することでコード実行

という流れになります。

提案手法

今回は記号とt, rのみで、数字を用いず記号難読化シェル芸を実現します。

数字を使わないために

従来の研究では2>&1のリダイレクトによってエラーメッセージから最初の文字を錬成する方法が一般的でした。
しかし、この標準エラー出力のリダイレクト自体がシェル芸でも有名なテクニックであるため、動作が想像されてしまうデメリットがあります。

標準エラー出力リダイレクト

この問題に対処するため、標準エラー出力をパイプするテクニックを用います。
|&は通常のパイプとは異なり、標準エラー出力を標準入力にパイプします。

$ ____=$(.|&tr +\(= -$\"%)

上記コマンドは、.コマンドの実行によって得られるエラーメッセージを|&でパイプし、trコマンドに繋いだ出力を変数$__に代入しています。
ここで、trコマンドの引数は特に問題ではないので、適当につけます。謎感を出すことと、できる限りのカモフラージュをするためにある程度の長さの記号列にしました。

そして、先行研究と同様の手法で、filename [arguments]のみを抽出します(厳密には文字列の先頭にスペースが入っており、echoでは先頭のスペースが切り落とされています)。

$ ____=${____##*.}
$ echo $____
filename [arguments]

プロセスIDによる数字獲得

数字を用いない手法のため、数字の取得が必須となりました。
ここで、確実に自然数を生成できる方法を紹介します。

Bashにはいくつか予約変数が提供されていますが、その1つに$$があります。
これには現在のプロセスIDが格納されています。
プロセスIDは0を割り当てられることがないので、$(($$/$$))によって確実に1を取得することができます。

$ __=$(($$/$$)) # 1
$ ___=$(($__+$__)) # 2

また、前のコマンドが成功していたら、$?(前コマンドの終了ステータス)には0が格納されています。前のコマンドが確実に成功することがわかっていれば有効に使うことができます。
今回は代わりに$(($$-$$))を用います。

文字の取得

前述の通り、記号難読化シェル芸では入力できる文字を大幅にカバーするためにls --helpを打つことが目標として設定しやすいです。しかし、ls --helpはGNU系においては確かに大量のヘルプメッセージを出力してくれますが、MacなどBSD系ではそもそも--helpオプションが存在しません。

$ ls --help
ls: illegal option -- -
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]

したがって、異なるアプローチでの取得を目指します。

シーケンス表現とUnicodeエスケープ

既に[1]でも採用されていますが、シーケンス表現を用いることにします。

$ echo {a..z}

しかし、今回zを得る手段が見つからなかったため、代わりにevalとUnicodeエスケープによって実現します。

$ eval echo $(echo -e {\u61..\u7a})

これには、まだ取得できていないc o vが必要です。

set始動

[2]ではWSLに収録されているmtコマンドから更なる文字列の取得を行っています(文献ではmt始動と記述されています)が、デフォルトのdebian環境ではmtコマンドが用意されていないため、代わりのコマンドを探します。

前述の通りGNUやBSDなどの差異によって出力が変わってはいけないため、今回はbashで予約されたコマンドから探します。

. : alias bg bind break builtin caller cd command compgen complete compopt continue declare dirs disown echo enable eval exec exit export fc fg getopts hash help history jobs kill let local logout mapfile popd printf pushd pwd read readarray readonly return set shift shopt source suspend test times trap type typeset ulimit umask unalias unset wait

この中からfilename [arguments]、すなわちaefgilmnrstuだけで打てるコマンドで、なおかつc o vを出してくれるコマンドを探さなくてはなりません。
今回白羽の矢が立ったのはsetコマンドです。

$ set -g
bash: set: -g: invalid option
set: usage: set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]

Cが大文字ですが、これは${VAR,,}によって小文字化することができるので問題ありません。
どのコマンドも引数無しでは欲しい文字を出してくれませんでしたが、わざと誤ったオプションを与えるといくつかのコマンドでusageを出してくれました。
.コマンド同様、usage部分は言語環境に依存しないため、これを抽出します。

# 実行されるコマンド:_____=$(set -g|&tr +\(= -$\"%)
# 数字以外難読化:_____=$(${____:19:1}${____:18:1} -${____:13:1}|&tr +\(= -$\"%)
# 難読化後:
$ _____=$(${____:$(($___$(($$-$$))-$__)):$__}${____:$___*$___:$__}${____:$(($___$(($$-$$))-$___)):$__} -${____:$__$(($__+$___)):$__}|&tr +\(= -$\"%)

# 実行されるコマンド:_____=${_____##*set}
# 難読化
$ _____=${_____##*${____:$(($___$(($$-$$))-$__)):$__}${____:$___*$___:$__}${____:$(($___$(($$-$$))-$___)):$__}}

# 小文字化
$ _____=${_____,,}

$ echo $_____
[-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]

続いてシーケンス表現によってすべての英字を取得します。

# 目的のコマンド:______=($(echo {a..z}))
# 実行されるコマンド:______=($(eval echo $(echo -e "{\u61..\u7a}")))
# 数字以外難読化:______=($(${____:4:1}${_____:14:1}${____:6:1}${____:3:1} ${____:4:1}${_____:17:1}${_____:7:1}${_____:24:1} $(${____:4:1}${_____:17:1}${_____:7:1}${_____:24:1} -${____:4:1} "{\\${____:14:1}61..\\${____:14:1}7${____:11:1}}")))

# 難読化後:
______=($(${____:$(($___*$___)):$__}${_____:$__$(($___*$___)):$__}${____:$(($___*$___+$___)):$__}${____:$(($___+$__)):$__} ${____:$(($___*$___)):$__}${_____:$(($___*$___*$___*$___+$__)):$__}${_____:$(($___*$___*$___-$__)):$__}${_____:$___$(($___*$___)):$__} $(${____:$(($___*$___)):$__}${_____:$(($___*$___*$___*$___+$__)):$__}${_____:$(($___*$___*$___-$__)):$__}${_____:$___$(($___*$___)):$__}  -${____:$(($___*$___)):$__} "{\\${____:$__$(($___*$___)):$__}$(($___*$___+$___))$__..\\${____:$__$(($___*$___)):$__}$(($___*$___*$___-$__))${____:$__$__:$__}}")))


# 目的のコマンド:______=($(echo {A..Z}))
# 実行されるコマンド:______=($(eval echo $(echo -e "{\u41..\u5a}")))
# 数字以外難読化:_______=($(${____:4:1}${_____:14:1}${____:6:1}${____:3:1} ${____:4:1}${_____:17:1}${_____:7:1}${_____:24:1} $(${____:4:1}${_____:17:1}${_____:7:1}${_____:24:1} -${____:4:1} "{\\${____:14:1}41..\\${____:14:1}5${____:11:1}}")))

# 難読化後:
_______=($(${____:$(($___*$___)):$__}${_____:$__$(($___*$___)):$__}${____:$(($___*$___+$___)):$__}${____:$(($___+$__)):$__} ${____:$(($___*$___)):$__}${_____:$(($___*$___*$___*$___+$__)):$__}${_____:$(($___*$___*$___-$__)):$__}${_____:$___$(($___*$___)):$__}  $(${____:$(($___*$___)):$__}${_____:$(($___*$___*$___*$___+$__)):$__}${_____:$(($___*$___*$___-$__)):$__}${_____:$___$(($___*$___)):$__}  -${____:$(($___*$___)):$__} "{\\${____:$__$(($___*$___)):$__}$(($___*$___))$__..\\${____:$__$(($___*$___)):$__}$(($___*$___+$__))${____:$__$__:$__}}")))

このように______=(a b c ...)とすることで、$______と$_______は文字の配列となります。これにより、以下のようにインデックスをつければ任意の文字を取得することができます。

$ echo ${______[@]}
a b c d e f g h i j k l m n o p q r s t u v w x y z
$ echo ${_______[0]}
A

任意コマンドの実行

これで任意の数字及び任意の英字を使える状態になったため、好きなコマンドを記号だけで実行できるようになりました。

# 実行されるコマンド:date
# 数字以外難読化:${______[3]}${______[0]}${______[19]}${______[4]}
# 難読化後:
${______[$(($__+$___))]}${______[$(($$-$$))]}${______[$(($___$(($$-$$))-$__))]}${______[$(($___*$___))]}

最終的なコマンド

$ __=$(($$/$$));___=$(($__+$__));____=$(.|&tr +\(= -$\"%);____=${____##*.};_____=$(${____:$(($___$(($$-$$))-$__)):$__}${____:$___*$___:$__}${____:$(($___$(($$-$$))-$___)):$__} -${____:$__$(($__+$___)):$__}|&tr +\(= -$\"%);_____=${_____##*${____:$(($___$(($$-$$))-$__)):$__}${____:$___*$___:$__}${____:$(($___$(($$-$$))-$___)):$__}};_____=${_____,,};______=($(${____:$(($___*$___)):$__}${_____:$__$(($___*$___)):$__}${____:$(($___*$___+$___)):$__}${____:$(($___+$__)):$__} ${____:$(($___*$___)):$__}${_____:$(($___*$___*$___*$___+$__)):$__}${_____:$(($___*$___*$___-$__)):$__}${_____:$___$(($___*$___)):$__} $(${____:$(($___*$___)):$__}${_____:$(($___*$___*$___*$___+$__)):$__}${_____:$(($___*$___*$___-$__)):$__}${_____:$___$(($___*$___)):$__}  -${____:$(($___*$___)):$__} "{\\${____:$__$(($___*$___)):$__}$(($___*$___+$___))$__..\\${____:$__$(($___*$___)):$__}$(($___*$___*$___-$__))${____:$__$__:$__}}")));_______=($(${____:$(($___*$___)):$__}${_____:$__$(($___*$___)):$__}${____:$(($___*$___+$___)):$__}${____:$(($___+$__)):$__} ${____:$(($___*$___)):$__}${_____:$(($___*$___*$___*$___+$__)):$__}${_____:$(($___*$___*$___-$__)):$__}${_____:$___$(($___*$___)):$__}  $(${____:$(($___*$___)):$__}${_____:$(($___*$___*$___*$___+$__)):$__}${_____:$(($___*$___*$___-$__)):$__}${_____:$___$(($___*$___)):$__}  -${____:$(($___*$___)):$__} "{\\${____:$__$(($___*$___)):$__}$(($___*$___))$__..\\${____:$__$(($___*$___)):$__}$(($___*$___+$__))${____:$__$__:$__}}")));${______[$(($__+$___))]}${______[$(($$-$$))]}${______[$(($___$(($$-$$))-$__))]}${______[$(($___*$___))]}
Wed Feb 20 15:11:00 UTC 2019

上記コマンドの実行により、現在時刻が表示されました。
具体的な内容と順序は以下の通りです。

# 1の生成
$ __=$(($$/$$))

# 2の生成
$ ___=$(($__+$__))

# .コマンドから' filename [arguments]'の生成
$ ____=$(.|&tr +\(= -$\"%)
$ ____=${____##*.}

# setコマンドから' [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]'の生成
$ _____=$(set -g|&tr +\(= -$\"%)
$ _____=${_____##*set}
$ _____=${_____,,}

# {小,大}文字配列の生成
$ ______=($(echo {a..z}))
$ _______=($(echo {A..Z}))
 
# dateの実行
$ ${______[3]}${______[0]}${______[19]}${______[4]}

まとめ

記号難読化シェル芸の研究を有用性で殴ってはいけません。

参考文献

  1. 記号と1,2,A,zでだけで作る難読化シェル芸, @kanata201612 https://raintrees.net/news/114
  2. 超・記号オンリー難読化シェル芸, @xztaityozx_001, https://www.slideshare.net/xztaityozx/ss-93177781

記号と英字2文字だけでbash」への5件のフィードバック

  1. tr じゃなくて dd や m4 を使えば英字をひとつ減らせますね。
    dd の場合 stderr に余計なのが出てきますが。
    m4 はさらに /???/???/?4 のようにワイルドカードでマッチさせれば英字はなくせます。

    あるいは $(.|&tr …) を $(.>&/???/??/$__) に変えてもいいです。
    何してるかは解読してみてください。

    ↓もっと複雑でbashの独自拡張を使ってない例
    http://ya.maya.st/d/201208a.html#s20120809_1
    だいぶ古いものなんで最近の環境では動かないかもですが。
    (少なくとも最新のOSXでは不可)

  2. 大事なポイントを忘れてました。
    tr はそれ単体で任意の文字を作り出せるチートコマンドなんで、
    これを使っていいなら非常に簡単に任意コマンドを実行できます。

    =$(tr !-\< `-{<<<%\”$((${#?}+${#?}+${#?}+${#?}+${#?}))\&);$

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です