2014/05/12
KAICHO: s_naray[at]yahoo[dot]co[dot]jp
※SPAM防止中

悪い暗号化ってどんなもの

■はじめに

本書では、吉里吉里/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) | ((ch & 0x55555555)<<1);
                  buf[i] = ch;
          }

でも、示したかった「ちょっと知ってる人なら、暗号化後のデータを見るだけで、 復号化プログラムを知らなくても復号可能である」ということはちゃんと示せたので よしとしよう。

■結論

ことほど左様に、暗号化方法がマズいと復号化は容易である。

暗号化されたデータが復号化されてしまうのは、以下の二つが原因だ。

  1. 暗号化後のデータを見るだけで復号化のヒントが得られてしまう
  2. 復号化プログラムを解析されてしまう

だとすると、これらを防がねばならない。どっちの方が被害が大きいかというと、 それはもう1.に他ならない。少なくとも、1.については極く簡単に対策できるので、 暗号化について考えている諸兄においては、少なくとも、データ見ただけで (復号化プログラムを知らなくても)復号化されちゃうような暗号化はやめましょうよ。

…という、本書はその啓発でありました。

あ、念のため言っとくと、我輩は別に吉里吉里のデフォルト暗号化が悪いと 言ってるわけじゃないよ。アレはセーブデータをユーザには見せなくして、 「ゲームを面白くするために」解析を防ぐのが目的だから、必要十分だと思う。 大事なのは、改竄防止のための暗号化は、こんな簡単なヤツじゃダメですよ、 というのを認識すること。