SECCON Beginners CTF 2020 write-up
Reversingのmask, siblangsの作問を担当したので、想定解を出しておきます
とりあえずお疲れ様でした
「わかる人」向けに詳細な説明を飛ばしてサクッと書いています。わからない場合はTwitterで @sei0o 宛に「わからん!」と送ってもらえれば内容を補充します! mask 62points
ざっくり触ってみましょう
コマンドライン引数にFLAGを与えられるようです。
code:bash
$ ./mask
とりあえず適当な文字列を渡してみると、よくわからない文字列と共に、「FLAGが違うよ」とメッセージが表示されます。
code:bash
./mask hogehogepiyopiyo123456
Putting on masks...
eeeeeepaqepaqe101454
hkcahkcaiikiik!"# !"
Wrong FLAG. Try again.
逆コンパイラなどを使って、プログラムの中身を解析します
main関数にすべて書かれています
コマンドライン引数でFLAGを受け取ると、各文字の文字コードと 0x75 (0b01110101)・0xeb (0b11101011) のANDをとります(マスク)
0x75 でマスクした結果が atd4\`qdedtUpetepqeUdaaeUeaqau 、0xeb でマスクすると c\`b bk\`kj\`KbababcaKbacaKiacki となるような文字列がFLAGになります
参考までに、元のソースコードを貼っておきます→Gist 逆コンパイル結果とソースコードを見比べてみてください
FLAGになりうる文字列を総当りしてマスクの結果を求めていくのは時間がかかるので、解析の結果とマスク演算の性質を使って元のFLAGを求めます。
あるASCII文字コード c を先出の 0x75 (0b01110101) でマスクすることを考えます
ビットが1のところ(下から1, 3, 5-7ビット目)
→ c の同じ位置のビットがそのまま結果になります
ビットが0のところは(下から2, 4, 8ビット目)
→ c のビットにかかわらず 0 が出力され、出力からは c のその位置のビットが0であったか1であったかはわかりません
ところが、0x75 (0b01110101) で0となっているビットは、もう一つのマスク 0xeb (0b11101011) ではすべて1となっているので、2つの結果を合わせることで c の文字コードを復元できます。
「合わせる」とはつまり、マスクした結果の2つの文字のORを取ることです。
式で表すと以下のようになります。flag_iはFLAGのi番目の文字の文字コードです。
code:txt
masked_a = flag_i AND 0x75
masked_b = flag_i AND 0xeb
0x75 OR 0xeb = 0b01110101 OR 0b11101011 = 0b11111111
masked_a OR masked_b
= (flag_i AND 0x75) OR (flag_i AND 0xeb)
= flag AND (0x75 OR 0xeb)
= flag_i AND 0b11111111
= flag_i
一番下の式の変形(分配法則)はベン図を描くとわかりやすいです。
siblangs 363points
Android → Java, React Native → JavaScript
siblings(兄弟) + lang で siblang
Androidアプリのapkが渡されます
APKの中身はzipファイルなので `$ unzip
Android端末を持っている方は一度インストールしてみてください
なくても解けます
ADBでインストールできます
アプリを開くとFLAGを入れる欄があり、「VALIDATE A」「VALIDATE B」のボタンが提示されています。
「VALIDATE A」のボタンを押すとiOSでアプリを動かすように言われます
が、iOSアプリは配布していないので当然動かせません
「VALIDATE B」のボタンを押すとバリデーションに失敗した旨の表示が出てきます
ヒント:「Learn once, write anywhere」はReact Nativeのキャッチコピーです
「VALIDATE B」について
とりあえずapkを展開・逆コンパイルしてみます
APKの解析はいろいろなツールがあるので、探してみてください
es.o0i.challengeapp.nativemodule名前空間の下に ValidateFlagModule というクラスが見つかります
validate関数
あらかじめAES-GCMで暗号化しておいたバイト列を、同一クラス内にある鍵で復号した結果と入力を比較し、一致していればコールバック関数にtrueを渡します。一致しなければfalseを渡します
これを使ってバイト列を復号すると、1pt_3verywhere}となり、FLAGの後半が得られます
23文字目以降に(これもValidateFlagModuleの処理からわかります) 1pt_3verywhere} が現れるように前半を適当に埋めて「VALIDATE B」を押すと、バリデーションに成功した旨が表示されます。
「VALIDATE A」について
「Learn once, write anywhere」や、逆コンパイル結果で得られるReact系ライブラリのインポートからReact Nativeを使用していることと推測が立ちます
ReactはWebフレームワークで、React NativeはReactと同じようにしてネイティブアプリを書けるようにしたフレームワークです
apkのassetsディレクトリには index.android.bundle というファイルがありますが、これはReact Nativeで使用されるJavaScriptソースを一つにまとめたものです。難読化をjs-beautifier等で解除してから、アプリ中の文言などを使って関係ある場所を探すと以下が見つかります。 code:js
...
for (var o = arguments.length, n = new Array(o), c = 0; c < o; c++) nc = argumentsc; return (t = y.call.apply(y, this.concat(n))).state = { flagVal: "ctf4b{",
xored: 34, 63, 3, 77, 36, 20, 24, 8, 25, 71, 110, 81, 64, 87, 30, 33, 81, 15, 39, 90, 17, 27 },
...
t.onPressValidateFirstHalf = function() {
if ("ios" === h.Platform.OS) {
for (var o = "AKeyFor" + h.Platform.OS + h.Platform.Version, l = t.state.flagVal, n = 0; n < t.state.xored.length; n++)
if (t.state.xoredn !== parseInt(l.charCodeAt(n) ^ o.charCodeAt(n % o.length), 10)) return void h.Alert.alert("Validation A Failed", "Try again..."); h.Alert.alert("Validation A Succeeded", "Great! Have you checked the other one?")
} else h.Alert.alert("Sorry!", "Run this app on iOS to validate! Or you can try the other one :)")
}, t.onPressValidateLastHalf = function() {
"android" === h.Platform.OS ? p.default.validate(t.state.flagVal, function(t) {
t ? h.Alert.alert("Validation B Succeeded", "Great! Have you checked the other one?") : h.Alert.alert("Validation B Failed", "Learn once, write anywhere ... anywhere?")
}) : h.Alert.alert("Sorry!", "Run this app on Android to validate! Or you can try the other one :)")
}, t
}
ちょっとこのままでは読めないので、インデントなどを適宜修正したものを以下に示します
iOSで実行した場合は以下の部分が実行されることになります(読みやすくしました)
入力を AKeyForios10.3 で一文字ずつループさせながらXORした結果がxoredの配列の中身に等しくなればよいので、xoredの中身をAKeyForios10.3 でXORしてやればしかるべき入力、すなわちFLAGの前半が手に入ります(ctf4b{jav4_and_j4va5cr)
ほんとはバージョンもなんらかのメカニズムで変えるつもりだったけれど、複雑すぎるのもよくないと思ってやめた
code:js
flagVal: "ctf4b{",
xored: 34, 63, 3, 77, 36, 20, 24, 8, 25, 71, 110, 81, 64, 87, 30, 33, 81, 15, 39, 90, 17, 27 ...
if ("ios" === h.Platform.OS) {
var o = "AKeyFor" + h.Platform.OS + "10.3",
l = t.state.flagVal;
for (n = 0; n < t.state.xored.length; n++)
if (t.state.xoredn !== parseInt(l.charCodeAt(n) ^ o.charCodeAt(n % o.length), 10)) return void h.Alert.alert("Validation A Failed", "Try again...");
h.Alert.alert("Validation A Succeeded", "Great! Have you checked the other one?")
}