鍵生成プログラムKeygenをPythonで作成します。 まずはradare2を用いてパスワード認証アルゴリズムを解析し、それを開くことができるKeygenを作成しました。
はじめに
おはよう。@bioerrorlogです。
前回は、ごく簡単な暗号アルゴリズムを用いてパスワード認証プログラム"your_pass_2"を作成しました。
今回は、このバイナリコードを自らリバースエンジニアリングして仕様を解析し、パスワード認証を通過できる鍵を生成する"Keygen"をPythonで作成します。
なお、内容はこのLiveOverflow氏の動画を参考に、自分で少し改変を加えつつ流れを再現するという形になります。
作業環境
Ubuntu18.04.1 LTS を
Windows10の上に、VMwareによって構築した仮想環境で起動しています。
www.bioerrorlog.work
Pythonで鍵生成
radare2でアルゴリズムを解析する
radare2でバイナリコード"your_pass_2"を逆アセンブルし、パスワード認証のアルゴリズムを解析します。 まずは、ビジュアルモードで着目すべき場所を探します。
なお、radare2の使い方については過去記事に詳しく記録しました。
それでは、さっそくradare2のビジュアルモードでmain関数を解析します。
$ r2 your_pass_2 ~ > aaa ~ > s sym.main > VV Rendering graph...
ビジュアルモードの結果を入念に見ると、一箇所だけループ構造が発生していました(Fig. 1)。 ここを詳しく解析してみます。
forループの特定
ループ構造として、まずはforループを疑ってみます。 これが典型的なforループならば、ループに入る前に0にセットされ、ループのたびに1ずつ増える変数(ループカウンタ" i ")があるはずです。 探してみると、ループ前に0にセットされている変数が2つありました(Fig. 1, ➀ブロック)。
mov dword [local_18h], 0 mov dword [local_14h], 0
この2つの変数dword [local_18h]
とdword [local_14h]
の行く末を見てみると、dword [local_14h]
はループごとに1が加算されているのがわかります(Fig. 1, ➂ブロック)。
add dword [local_14h], 1
ここからdword [local_14h]
がループカウンタ" i "である期待が高まります。
つぎに、dword [local_14h]
がループカウンタであれば、forループの終了条件に絡んでいるはずです。
そこでforループの分岐条件を辿ってみると(Fig. 1, ➁)、
mov eax, dword [local_14h]
~
movsxd rbx, eax
~
cmp rbx, rax
jb 0x723
dword [local_14h]
→eax
→rbx
と、分岐条件cmp rbx, rax
に繋がっていました。
以上のことから、このループ構造はdword [local_14h]
をループカウンタ" i "としたforループであると判断しました。
forループの処理を解析する
次に、forループ内の処理(Fig. 1, ➂ブロック)を解析しました。 とても複雑で難解なので、Cで書かれたソースコード1を書き置きます。
#include <string.h> #include <stdio.h> int main(int argc, char *argv[]) { if(argc==2) { printf("Checking Your Pass: %s\n", argv[1]); int sum = 0; for (int i=0; i<strlen(argv[1]); i++) { sum += (int)argv[1][i]; } if(sum==5345) { printf("Hello, Master.\n"); } else { printf("Access Denied.\n"); } } else { printf("Access Denied.\n"); } return 0; }
このソースコードによると、forループ内では、入力された文字argv[1]
を一文字ずつint値として変数sumに加算していることがわかります。
この処理を、radare2で逆アセンブルしたコードから読み解きます。
forループ内(Fig. 1, ➂ブロック)は、
mov rax, qword [s] add rax, 8 mov rdx, qword [rax] mov eax, dword [local_14h] cdqe add rax, rdx movzx eax, byte [rax] movsx eax, al add dword [local_18h], eax add dword [local_14h], 1
となっています。 最初から順に読み解いていきます
まず最初の2行に着目しました。
mov rax, qword [s]
add rax, 8
レジスタrax
に値が格納された後、8が加算されている点に注目します。
これは、rax
が配列のアドレスを格納していることを示唆しています。
通常、64 bitコンピュータではメモリが8 byteずつに分割されています。
よって、1行目でrax
に何かの配列のアドレスが格納された場合、2つ目の配列要素にアクセスしたいときにはrax
に8を加算すればよい、というわけです。
ここで「2つ目の配列要素にアクセス」ということから考えられるのは、入力文字argv[1]
でしょう。
argv[]
配列は、プログラムの実行時に渡される引数を格納しています。
たとえばこのプログラム"your_pass_2"を、"Wrong-Pass"という文字列を渡して実行した場合、
$ ./your_pass_2 Wrong-Pass
argv[]
配列は次のように格納されています。
./your_pass_2 |
Wrong-Pass |
---|---|
argv[0] |
argv[1] |
よって、上記の2行は次のように解釈できそうです。
アセンブラコード | レジスタの解釈 |
---|---|
mov rax, qword [s] |
rax : argv[0] のアドレス |
add rax, 8 |
rax : argv[1] のアドレス |
では、次のコードに移ります
mov rdx, qword [rax] mov eax, dword [local_14h] cdqe add rax, rdx
前述の通り、rax
はargv[1]
のアドレスを示しているので、1行目でrdx
に格納されたのはargv[1]
の中身を指すアドレスでしょう。
通常、先頭が指定されます。
アセンブラコード | レジスタの解釈 |
---|---|
mov rdx, qword [rax] |
rdx : argv[1] の中身の先頭を指すアドレス |
次の行mov eax, dword [local_14h]
では、dword [local_14h]
が登場します。
これは、上述したようにforループで1ずつ増えるループカウンタです。
よってループカウンタの値がeax
に格納されるわけです。
その次の行cdqe
は、eax
をrax
に設定する操作です2。
よって最後のadd rax, rdx
は、先頭を指すアドレスrdx
に、ループカウンタの値rax
を足して、rax
に格納しています。
ここまでをまとめると、
アセンブラコード | レジスタの解釈 |
---|---|
mov eax, dword [local_14h] |
eax : ループカウンタの値 |
cdqe |
rax : ループカウンタの値 |
add rax, rdx |
rax : 先頭アドレス+ループカウンタ |
それでは最後の部分です。
movzx eax, byte [rax] movsx eax, al add dword [local_18h], eax
まずmovzx eax, byte [rax]
で、rax
の指し示すアドレスから1バイトを読み込んでいます。
この値はまさに入力文字のASCII値であると考えられます。
例えば、入力文字が"Wrong-Pass"で、ループカウンタが2の場合、
入力文字: | W | r | o | n | g |
---|---|---|---|---|---|
ASCII値: | 0x57 | 0x72 | 0x6F | 0x6E | 0x67 |
rax(rdx+ループカウンタ): | rdx+1 | rdx+2 | rdx+3 | rdx+4 | rdx+5 |
eax
には"r"のASCII値0x72が格納されるというわけです。
次の行movsx eax, al
では、eaxの下位8ビット部分がeax
に再格納されています。
というのも、al
はeax
の下位8ビットを示すレジスタだからです3。
そして最後には、add dword [local_18h], eax
でeax
の値がdword [local_18h]
に加算されています。
ここで、先に述べたようにdword [local_18h]
は0で初期化された変数です。
つまり、ループごとにdword [local_18h]
に入力文字が加算され、最後には入力文字の合計ASCII値の総和が格納されている、ということになります。
かなり長く複雑な解析をしてきました。 ここまでわかったforループの処理をまとめます。
アセンブラコード | レジスタの解釈 |
---|---|
mov rax, qword [s] |
rax : argv[0] のアドレス |
add rax, 8 |
rax : argv[1] のアドレス |
mov rdx, qword [rax] |
rdx : argv[1] の中身の先頭を指すアドレス |
mov eax, dword [local_14h] |
eax : ループカウンタの値 |
cdqe |
rax : ループカウンタの値 |
add rax, rdx |
rax : 先頭アドレス+ループカウンタ |
movzx eax, byte [rax] |
eax : 入力文字のASCII値 |
movsx eax, al |
eax : 下位8ビットを再格納 |
add dword [local_18h], eax |
dword [local_18h] : 入力文字ASCII値の総和 |
アクセス可否の判定条件
ここまで、forループ内で入力文字の総和dword [local_18h]
が計算されていることを明らかにしました。
つぎは、この処理結果がどのようにしてアクセス可否の判定に使われているかを調べます。
これは簡単に分かりました。 forループ終了後の処理を見ると(Fig. 1, ➃)、次のようになっています。
cmp dword [local_18h], 0x14e1 jne 0x778
つまり、入力文字の総和dword [local_18h]
が0x14e1であるかどうかを判定し、プログラムが分岐しているわけです。
ついに、このプログラムの挙動が明らかになりました。 各入力文字ASCII値の総和が0x14e1であればアクセス承認、そうでなければアクセス拒否です。
Pythonで鍵生成 - Keygen
このパスワード認証プログラムの仕組みがわかったので、これを破るためのKeygenを作成します。 仕様は単純です。 ASCII値の総和が0x14e1となる文字列を自動生成するプログラムを、Pythonで書きます4。
import random #文字列keyの合計ASCII値を算出する関数 def check_key(key): char_sum = 0 for c in key: char_sum += ord(c) return char_sum #合計ASCII値が5345(0x14e1)である文字列を無限ループで生成 key = "" while True: key += random.choice("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_") s = check_key(key) if s > 5345: key = "" elif s == 5345: print ("Key: {0}".format(key))
さっそくこのPythonプログラム"keygen_your_pass_2.py"実行してみます。
$ python3 keygen_your_pass_2.py Key: 3beg-iCw2Xg7OP7E1xkn-HEaxai-KRl43pj5OkJuViZJnIozP9-q-XKOG7v0vWy6 Key: kI_MoMol9yqM6gImi2lyfVhNn6npOo7eRXxelQ4VHh88_BIfb5NtKvsyVHF1 Key: 9qFkJQVe74CTJz5EpF8h6E6bukw1XFYJ7YhndKM58SlymLooMra30RfgUUFUN5kK Key: PLySl4Mu_eDoOaDQ7BV09K6WGrtOZFLP--pojSBFq8GPxIsgIxX_I0YkNQmBb8f7 Key: ui-IAbPFU2cR20ZMCMYSbyMqbuNpUqNutqwtll-YxeZx83KcI--u-PJF5B3RuAO Key: ZL6AZgMIlphS0spzgWEsy_sFKusCS_46ahjGm2T7PQBf7h3ifrmytkX89s5z Key: LfX9lEUp5vGDTVOlo2O7NyJQR_yne6Dd8BBTCQxfBleMmIz8RJPYYa9JuGpfdg Key: 9yMjf2YKivb84KuhF6iLdvTssguiQne6kVKyjJvAvlV13qskhNgfLOHQBy6 Key: 5tzqUevjCYFoZ1lEnJWoIJJOHD4OMTv3qB5A3W2SBlHlhuXThrqksLV8FuHc-j Key: _mcHUIbIM5QEAyGQOTqvpY-5TKsVBJEdGuT86fLcP3KrqAUH_fD1s0fR2JNqVS0NL Key: 6t0pv0F3SbtKXwEjsp47xrfnC9zr2VjtqTOrTEYGcmwoRvpkdBT2bj_T6dO Key: BjOLf49V_xPvaONmPyiyKoQAP0WYc3PNrq7VD-UQqT8I3otgILvmADOrGo3sDf Key: nNBYpcTZAXdx8aXA5EUwSgXsKT2lcQpaH7JnuEuvmO2bvvXKNkSdwVPlkIm Key: YnvH_JbZsDwKmJg7_fA4xnrn-UwM_d_N_G-xQS47wsOI5q12Cwc9hVIx_m3C3D #以下略
即座に大量の鍵が生成されました。 ちなみに無限ループなので、処理を終了するには"Ctrl+c"を押す必要があります。
それでは、はたしてこれらの鍵でこのプログラム"your_pass_2"のアクセス承認が引き出せるのかを確かめます。
$ ./your_pass_2 A2viBkKm4IU-I_pEGF6-7YKONqAEk1Vl6KIAAg88fHKjFMN2DAqBjBvlwaqib-_xe5 Checking Your Pass: A2viBkKm4IU-I_pEGF6-7YKONqAEk1Vl6KIAAg88fHKjFMN2DAqBjBvlwaqib-_xe5 Hello, Master.
"Hello, Master."
見事、アクセス承認を引き出すことができました。
おわりに
今回は、radare2を用いてパスワード認証アルゴリズムを解析し、それに基づいてPythonで鍵生成プログラムKeygenを作成しました。
以前のようにデバッグ上でレジスタを操作する5のでなく、完全に外側からアクセスを開くのはなかなか趣深いものでした。
今回はソースコードを知っている状態で、かつ極めて簡単な暗号アルゴリズムをリバースしましたが、自分が知らないプログラムの解析にも挑戦してみたいです。 そういうことをするための安全なコミュニティがネット上にはたくさんある6ようなので、今後手を出していけたらと思っています。
[関連記事]
-
http://milkpot.sakura.ne.jp/note/x86.htmlより、"eax は rax の下位 32 ビット部分を指す。 また ax は eax の下位 16 ビット部分を指す。 さらに ax は上下 2 つの 8 ビットレジスタ ah, al に分割して使うことができる。"↩
-
GitHub - LiveOverflow/liveoverflow_youtube: Material for the YouTube seriesを参考にしています。↩
-
レジスタ操作でアクセス承認を引き出す - gdb | リバースエンジニアリング入門#3 - 生物系がゼロから始めるTech Blog↩
-
逆に、禁じられているソフトウェアを解析することは違法ですから、そこの一線は絶対に超えてはなりません。↩