本書では、吉里吉里/KAGの暗号化されたデータを、クラッカーがどのように 復号化するのかを具体的に紹介し、悪い暗号化とは何かを示す。
なんとなくあるゲームの.xp3を展開したら、data/scenario/first.ksが以下のような バイナリデータだったとしよう。おおぅ暗号化されておるなぁ。いいぞいいぞ!
とあるゲームのfirst.ks |
---|
00000000 fe fe 01 ff fe 37 10 42 5a 41 72 41 72 41 56 41 |.....7.BZArArAVA| 00000010 db 43 99 43 9f 43 91 43 8f 6b b0 60 ee 61 d4 42 |.C.C.C.C.k.`.a.B| 00000020 81 68 e0 6a b5 68 ca 41 ca 41 ce 62 a3 41 c8 43 |.h.j.h.A.A.b.A.C| 00000030 a1 43 42 43 63 43 9b 43 82 43 8a 43 9b 41 7b 41 |.CBCcC.C.C.C.A{A| 00000040 d6 41 72 41 c9 10 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e |.ArA............| 00000050 1e 1e 1e 0e 05 37 10 43 99 43 9f 43 91 43 8f 43 |.....7.C.C.C.C.C| 00000060 8a 43 81 43 63 43 94 43 8a 41 f0 47 8d 63 5d 41 |.C.CcC.C.A.G.c]A| 00000070 7b 41 d6 41 56 41 cb 41 58 41 56 42 81 47 9b 6b |{A.AVA.AXAVB.G.k| 00000080 51 47 9b 6b 51 a5 41 ca 41 ce 6a ac 4d 59 41 ca |QG.kQ.A.A.j.MYA.| 00000090 41 57 41 c4 41 51 41 cc 41 ca 43 a4 43 8c 43 91 |AWA.AQA.A.C.C.C.| 000000a0 43 b9 0e 05 a7 96 99 10 9a b4 b0 3e 11 a3 b6 b3 |C..........>....| 000000b0 b8 9a 9e 1d b9 9a b1 b3 96 9f 9d 86 9d 99 9f b1 |................| 000000c0 9e 92 b8 96 9f 9d 1d b3 ba 91 b3 b8 b1 14 32 32 |..............22| 000000d0 1c 32 16 10 12 3e 10 1b a5 1b 11 ae 0e 05 06 a7 |.2...>..........| 000000e0 9a b9 92 9c 10 9a b4 b0 3e 11 88 9a 91 ba 9b 1d |........>.......| 000000f0 93 9f 9d b3 9f 9c 9a 1d b9 96 b3 96 91 9c 9a 10 |................| 00000100 3e 10 b8 b1 ba 9a 11 ae 0e 05 06 a7 9a b9 92 9c |>...............| 00000110 10 9a b4 b0 3e 11 88 9a 91 ba 9b 1d 93 9f 9d b8 |....>...........| 00000120 b1 9f 9c 9c 9a b1 1d b9 96 b3 96 91 9c 9a 10 3e |...............>| 00000130 10 b8 b1 ba 9a 11 ae 0e 05 a7 9a 9d 98 96 99 ae |................| 00000140 0e 05 0e 05 37 10 43 b0 43 91 43 90 41 f0 43 9a |....7.C.C.C.A.C.| 00000150 43 a4 43 9b 41 7b 41 d6 41 7e 41 ef 41 cc b0 92 |C.C.A{A.A~A.A...| 00000160 b8 93 94 b8 9a b3 b8 43 99 43 81 43 4c 43 8d 43 |.......C.C.CLC.C| 00000170 9b 43 45 61 cb 46 c2 0e 05 37 a7 9a b9 92 9c 10 |.CEa.F...7......| 00000180 9a b4 b0 3e 11 a3 b8 9f b1 92 9b 9a b3 1d 92 98 |...>............| 00000190 98 82 ba b8 9f a0 92 b8 94 14 1b b0 92 b8 93 94 |................| 000001a0 b8 9a b3 b8 1f 1b 16 11 ae 0e 05 0e 05 37 10 43 |.............7.C| 000001b0 8b 43 46 42 a7 4d 6d 41 cc 47 63 63 5d 60 ee 61 |.CFB.MmA.Gcc]`.a| 000001c0 d4 42 81 30 10 41 c4 41 db 43 8b 43 46 42 a7 4d |.B.0.A.A.C.CFB.M| 000001d0 6d 61 d1 4d bd 42 82 32 10 41 7d 41 c9 46 c1 68 |ma.M.B.2.A}A.F.h| 000001e0 ac 41 c4 41 db 43 8b 43 46 42 a7 69 73 4d 47 41 |.A.A.C.CFB.isMGA| 000001f0 7b 41 d6 0e 05 a7 9a b9 92 9c 10 9a b4 b0 3e 11 |{A............>.| 00000200 b3 99 1d 9a 9e b8 b6 b0 9a 10 3e 10 32 11 ae 0e |..........>.2...| 00000210 05 37 10 42 53 41 72 41 72 41 ec 41 ca 43 99 43 |.7.BSArArA.A.C.C| 00000220 9f 43 91 43 8f 6b b0 60 ee 61 d4 1e 1e 1e 1e 1e |.C.C.k.`.a......| 00000230 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e |................| 00000240 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e |................| 00000250 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 0e |................| 00000260 05 0e 05 0e 05 37 10 42 5a 43 a2 42 a7 43 40 62 |.....7.BZC.B.C@b| 00000270 8f 4f 44 6b 6e 45 85 4d 9d 10 1e 1e 1e 1e 1e 1e |.ODknE.M........| 00000280 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e |................| 00000290 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e |................| 000002a0 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e |................| 000002b0 1e 1e 1e 0e 05 0e 05 37 10 62 cc 4c 72 68 ca 41 |.......7.b.Lrh.A| 000002c0 c9 60 77 6a 96 68 ca 41 cc 47 d9 6a c5 41 f0 41 |.`wj.h.A.G.j.A.A| 000002d0 7b 41 d6 43 b8 43 46 43 8f 0e 05 37 10 b3 99 1d |{A.C.CFC...7....| 000002e0 9b 92 9e 9a b8 b6 b0 9a 10 3e 10 62 cc 4c 72 68 |.........>.b.Lrh| (snip) |
だが、ぱっと見ただけでこの暗号化はダメのダメダメだということがわかる。 人力で復号化できちゃいそうだからだ。なぜそう思ったかという理由は以下の通り。
正直、バイナリ眺めただけでここまでわかっちゃうと、 あとは力ワザでも復号化可能だ。 ははは、まさかそんな、その程度で復号化できるわけないじゃん、と タカをくくっちゃってる甘々なアナタのため、 じゃぁ復号化してやろうじゃないの!ということでやってみる。 我輩の本気見とけ!キィッ!
さすがにShift+F4でコンソールは出てこなかった。が、吉里吉里というバイナリが 我輩の手元に存在する以上、そんなものはなんとでもなるわな!…というか、 まぁ今回は展開したデータ一式が手元にあるので、じゃぁエラー起こして強制的に ログ吐かせりゃいいでしょ、と誰しもが思いつく。ズバリ、ファイルを一つ消して みればいいのだ。そしてエラーを起こさせたら、savedata/krkr.console.logを 見てみる。
(snip) 10:53:05 Scenario loaded : pluginsample.ks 10:53:05 pluginsample.ks : 10:53:05 pluginsample.ks : [macro name="start_link"] 10:53:05 pluginsample.ks : [eval exp="mp.storage = kag.conductor.curStorage" cond="mp.storage === void"] 10:53:05 pluginsample.ks : [eval exp="mp.exp = 'tf.target = ' + '\'' + mp.target + '\', tf.storage = ' + '\'' + mp.storage + '\''"] 10:53:05 pluginsample.ks : [eval exp="mp.storage = 'pluginsample.ks'"] 10:53:05 pluginsample.ks : [eval exp="mp.target = '*procedure_after_every_link'"] 10:53:05 pluginsample.ks : [link *] 10:53:05 pluginsample.ks : [endmacro] 10:53:05 macro : start_link : [eval exp="mp.storage = kag.conductor.curStorage" cond="mp.storage === void"][eval exp="mp.exp = 'tf.target = ' + '\'' + mp.target + '\', tf.storage = ' + '\'' + mp.storage + '\''"][eval exp="mp.storage = 'pluginsample.ks'"][eval exp="mp.target = '*procedure_after_every_link'"][link *] 10:53:05 pluginsample.ks : 10:53:05 pluginsample.ks : [macro name="end_link"] 10:53:05 pluginsample.ks : [endlink] 10:53:05 pluginsample.ks : [endmacro] 10:53:05 macro : end_link : [endlink] 10:53:05 pluginsample.ks : 10:53:05 pluginsample.ks : 10:53:05 pluginsample.ks : ラベル/ページ : *プラグインサンプル_loop:2/プラグインサンプル_loop 10:53:05 pluginsample.ks : [image storage=_24 layer=base page=fore] 10:53:05 ==== An exception occured at kaglayer.tjs(129)[(function) loadImages], VM ip = 15 ==== (snip) ファイル : pluginsample.ks 行 : 18 タグ : image ( ← エラーの発生した前後のタグを示している場合もあります ) file://./c/users/game/data/_24 について適切な拡張子を持ったファイルを見つけられませんでした |
よしよし、ログが残ってる残ってる。何を見るかというと、pluginsample.ksという ファイルの先頭のKAGスクリプトがこうなってるんだなー、という部分。これが判ると、 「暗号化された後のデータ」と「復号化されたテキスト」を 見比べることができるからだ。
さて、では暗号化されてるpluginsample.ksというのを見てみよう。…最初の5byteは first.ksと同じなので、これは多分ヘッダなんだろう、とアタリをつける。 こちらにも0x0e 0x05が連続してるから、間違いなくこの2byteは改行コードだ。 しかし、0x0e→0x0d、0x05→0x0aに変換するようなxor値は存在しないので、 だとするとこれは1byte xorではないな、ということがわかる。単純な加算・減算でも このような変換を施すことはできないから、暗号化方法は少し捻ってあるようだ。
00000000 fe fe 01 ff fe 15 43 b9 43 46 43 8f 43 83 43 63 |......C.CFC.C.Cc| 00000010 43 a8 43 63 43 b9 43 47 af 9c 9f 9f b0 0e 05 0e |C.CcC.CG........| 00000020 05 a7 9e 92 93 b1 9f 10 9d 92 9e 9a 3e 11 b3 b8 |............>...| 00000030 92 b1 b8 af 9c 96 9d 97 11 ae 0e O5 37 10 41 72 |............7.Ar| 00000040 41 d5 41 7d 41 c9 44 f4 60 68 9a b4 b0 41 ce 4d |A.A}A.D.`h...A.M| 00000050 9b 41 59 41 c4 41 5e 41 c4 41 c2 41 c8 41 7a 41 |.AYA.A^A.A.A.AzA| 00000060 ec 41 58 41 55 42 82 41 ec 41 6f 41 51 41 51 41 |.AXAUB.A.AoAQAQA| 00000070 ca 41 7a 41 da 42 81 0e 05 a7 9a b9 92 9c 10 9a |.AzA.B..........| 00000080 b4 b0 3e 11 9e b0 1d b3 b8 9f b1 92 9b 9a 10 3e |..>............>| 00000090 10 97 92 9b 1d 93 9f 9d 98 ba 93 b8 9f b1 1d 93 |................| 000000a0 ba b1 a3 b8 9f b1 92 9b 9a 11 10 93 9f 9d 98 3e |...............>| 000000b0 11 9e b0 1d b3 b8 9f b1 92 9b 9a 10 3e 3e 3e 10 |............>>>.| 000000c0 b9 9f 96 98 11 ae 0e 05 a7 9a b9 92 9c 10 9a b4 |................| 000000d0 b0 3e 11 9e b0 1d 9a b4 b0 10 3e 10 1b b8 99 1d |.>........>.....| 000000e0 b8 92 b1 9b 9a b8 10 3e 10 1b 10 17 10 1b ac 1b |.......>........| 000000f0 1b 10 17 10 9e b0 1d b8 92 b1 9b 9a b8 10 17 10 |................| 00000100 1b ac 1b 1c 10 b8 99 1d b3 b8 9f b1 92 9b 9a 10 |................| 00000110 3e 10 1b 10 17 10 1b ac 1b 1b 10 17 10 9e b0 1d |>...............| 00000120 b3 b8 9f b1 92 9b 9a 10 17 10 1b ac 1b 1b 11 ae |................| 00000130 0e 05 a7 9a b9 92 9c 10 9a b4 b0 3e 11 9e b0 1d |...........>....| 00000140 b3 b8 9f b1 92 9b 9a 10 3e 10 1b b0 9c ba 9b 96 |........>.......| 00000150 9d b3 92 9e b0 9c 9a 1d 97 b3 1b 11 ae 0e 05 a7 |................| 00000160 9a b9 92 9c 10 9a b4 b0 3e 11 9e b0 1d b8 92 b1 |........>.......| 00000170 9b 9a b8 10 3e 10 1b 15 b0 b1 9f 93 9a 98 ba b1 |....>...........| 00000180 9a af 92 99 b8 9a b1 af 9a b9 9a b1 b6 af 9c 96 |................| 00000190 9d 97 1b 11 ae 0e 05 a7 9c 96 9d 97 10 15 ae 0e |................| 000001a0 05 a7 9a 9d 98 9e 92 93 b1 9f ae 0e 05 0e 05 a7 |................| 000001b0 9e 92 93 b1 9f 10 9d 92 9e 9a 3e 11 9a 9d 98 af |..........>.....| 000001c0 9c 96 9d 97 11 ae 0e 05 a7 9a 9d 98 9c 96 9d 97 |................| 000001d0 ae 0e 05 a7 9a 9d 98 9e 92 93 b1 9f ae 0e 05 0e |................| 000001e0 05 0e 05 15 43 b9 43 46 43 8f 43 83 43 63 43 a8 |....C.CFC.C.CcC.| 000001f0 43 63 43 b9 43 47 af 9c 9f 9f b0 bc 43 b9 43 46 |CcC.CG......C.CF| |
さて、ここまでで改行コードは0x0e 0x05なことはわかった。次にどうするかと いうと、上のバイナリデータを、一行ごとに並べる。並べたものは以下の通り。
fe fe 01 ff fe 15 43 b9 43 46 43 8f 43 83 43 63 43 a8 43 63 43 b9 43 47 af 9c 9f 9f b0 0e 05 0e 05 a7 9e 92 93 b1 9f 10 9d 92 9e 9a 3e 11 b3 b8 92 b1 b8 af 9c 96 9d 97 11 ae 0e 05 37 10 41 72 41 d5 41 7d 41 c9 44 f4 60 68 9a b4 b0 41 ce 4d 9b 41 59 41 c4 41 5e 41 c4 41 c2 41 c8 41 7a 41 ec 41 58 41 55 42 82 41 ec 41 6f 41 51 41 51 41 ca 41 7a 41 da 42 81 0e 05 a7 9a b9 92 9c 10 9a b4 b0 3e 11 9e b0 1d b3 b8 9f b1 92 9b 9a 10 3e 10 97 92 9b 1d 93 9f 9d 98 ba 93 b8 9f b1 1d 93 ba b1 a3 b8 9f b1 92 9b 9a 11 10 93 9f 9d 98 3e 11 9e b0 1d b3 b8 9f b1 92 9b 9a 10 3e 3e 3e 10 b9 9f 96 98 11 ae 0e 05 a7 9a b9 92 9c 10 9a b4 b0 3e 11 9e b0 1d 9a b4 b0 10 3e 10 1b b8 99 1d b8 92 b1 9b 9a b8 10 3e 10 1b 10 17 10 1b ac 1b 1b 10 17 10 9e b0 1d b8 92 b1 9b 9a b8 10 17 10 1b ac 1b 1c 10 b8 99 1d b3 b8 9f b1 92 9b 9a 10 3e 10 1b 10 17 10 1b ac 1b 1b 10 17 10 9e b0 1d b3 b8 9f b1 92 9b 9a 10 17 10 1b ac 1b 1b 11 ae 0e 05 a7 9a b9 92 9c 10 9a b4 b0 3e 11 9e b0 1d b3 b8 9f b1 92 9b 9a 10 3e 10 1b b0 9c ba 9b 96 9d b3 92 9e b0 9c 9a 1d 97 b3 1b 11 ae 0e 05 a7 9a b9 92 9c 10 9a b4 b0 3e 11 9e b0 1d b8 92 b1 9b 9a b8 10 3e 10 1b 15 b0 b1 9f 93 9a 98 ba b1 9a af 92 99 b8 9a b1 af 9a b9 9a b1 b6 af 9c 96 9d 97 1b 11 ae 0e 05 a7 9c 96 9d 97 10 15 ae 0e 05 a7 9a 9d 98 9e 92 93 b1 9f ae 0e 05 0e 05 a7 9e 92 93 b1 9f 10 9d 92 9e 9a 3e 11 9a 9d 98 af 9c 96 9d 97 11 ae 0e 05 a7 9a 9d 98 9c 96 9d 97 ae 0e 05 a7 9a 9d 98 9e 92 93 b1 9f ae 0e 05 0e 05 0e 05 |
最初の5byteはヘッダなので無視するとして、やっぱり行頭は0x15か0xa7、0x37など 共通なのが多い。注目は0xa7。この行の末尾は、必ず0xaeとなっていることに気づいた だろうか。このように、行頭と行末が対応するといえば、KAGのタグだ! ということは、ほらもう判った!0xa7='['、0xae=']'だ!
そこまで判ったら、今度はkrkr.console.logの出力をアテにする。 最初が '[macro name="start_link"]' だったから、文字数を元に、これと 合致する行を上から探せばいい。えーと…三行目かな。
[ m a c r o n a m e = " s t a r t _ l i n k " ] a7 9e 92 93 b1 9f 10 9d 92 9e 9a 3e 11 b3 b8 92 b1 b8 af 9c 96 9d 97 11 ae 0e 05 |
ハハン!複数出てくる m(=0x9e) や a(=0a92)、r(=0xb1) などが全て一致するので、 間違いない、この暗号化は少なくともバイト単位で同一のキーが使われている ことがわかった。
同じように、krkr.console.logと上のを比較すると、アルファベットは以下のように 暗号化されていることが判明した。
0x92 = 'a' 0x91 = 'b' 0x93 = 'c' 0x98 = 'd' 0x9a = 'e' 0x99 = 'f' 0x9b = 'g' 0x94 = 'h' 0x96 = 'i' 0x95 = 'j' 0x97 = 'k' 0x87 = 'K' 0x9c = 'l' 0x9e = 'm' 0x9d = 'n' 0x8d = 'N' 0x9f = 'o' 0xb0 = 'p' 0xa0 = 'P' 0xb2 = 'q' 0xa2 = 'Q' 0xb1 = 'r' 0xb3 = 's' 0xb8 = 't' 0xa8 = 'T' 0xba = 'u' 0xb9 = 'v' 0xbb = 'w' 0xb4 = 'x' 0xb6 = 'y' 0x?? = 'z' |
さて、ここまでできたら、もうやってた人にはピンとくる。我輩はピンときた。 何故かというと、上の縦に並べたパターンの下位4bitが、 以下のように循環しているから。
2→1→3→8→a→9→b→4→6→5→7→c→e→d→f→0→2
よーく見ると、上位4bitも、9→b、8→a のようになっていて、これはこの 循環にバッチリ含まれている。ということは、…4bitスクランブルだッ! 間違いないッ!
ということで、書いた復号化プログラムがこちら。
#!/usr/bin/perl -w sub xfr { my @xfrary = ( 0, 2, 1, 3, 8, 10, 9, 11, 4, 6, 5, 7, 12, 14, 13, 15 ); return $xfrary[$_[0] & 0x0f]; } binmode(STDIN); read(STDIN, $header, 5); $header = unpack("H10", $header); if ($header ne "fefe01fffe") { print "This is not the file(header = $header). Skip.\n"; exit 1; } while (read(STDIN, $byte, 1) == 1) { my $c = unpack("C", $byte); $c = (xfr($c >> 4)<<4) + xfr($c); $byte = pack("C", $c); print $byte; } |
このプログラムで復号化したpluginsample.ksがこちら。…ちうかまぁ、 実はコレは我輩が書いたスクリプトなんだけどね。
*プラグインサンプル_loop [macro name="start_link"] ; これだと引数expは使えなくなってしまうが、まぁいいでしょ。 [eval exp="mp.storage = kag.conductor.curStorage" cond="mp.storage === void"] [eval exp="mp.exp = 'tf.target = ' + '\'' + mp.target + '\', tf.storage = ' + '\ '' + mp.storage + '\''"] [eval exp="mp.storage = 'pluginsample.ks'"] [eval exp="mp.target = '*procedure_after_every_link'"] [link *] [endmacro] [macro name="end_link"] [endlink] [endmacro] *プラグインサンプル_loop|プラグインサンプル_loop (snip) |
というわけで、ちょっとしたヒントだけで、バッチリ復号化できた。これはまぁ 簡単な部類ですな。
実はこの暗号化は、吉里吉里がデフォルトで用意している暗号化なのでした。 system/Config.tjsでsaveDataMode='c'で使われるというアレ。 tTVPTextReadStream.Read()で実装されている復号化プログラムは以下の通り。 正確には4bitスクランブルではなくて、2bitごとにbitを入れ替えるという 暗号化なのでした。
// simple crypt for(tjs_uint i = 0; i |
でも、示したかった「ちょっと知ってる人なら、暗号化後のデータを見るだけで、 復号化プログラムを知らなくても復号可能である」ということはちゃんと示せたので よしとしよう。
ことほど左様に、暗号化方法がマズいと復号化は容易である。
暗号化されたデータが復号化されてしまうのは、以下の二つが原因だ。
だとすると、これらを防がねばならない。どっちの方が被害が大きいかというと、 それはもう1.に他ならない。少なくとも、1.については極く簡単に対策できるので、 暗号化について考えている諸兄においては、少なくとも、データ見ただけで (復号化プログラムを知らなくても)復号化されちゃうような暗号化はやめましょうよ。
…という、本書はその啓発でありました。
あ、念のため言っとくと、我輩は別に吉里吉里のデフォルト暗号化が悪いと 言ってるわけじゃないよ。アレはセーブデータをユーザには見せなくして、 「ゲームを面白くするために」解析を防ぐのが目的だから、必要十分だと思う。 大事なのは、改竄防止のための暗号化は、こんな簡単なヤツじゃダメですよ、 というのを認識すること。