banner
lca

lca

真正的不自由,是在自己的心中设下牢笼。

《ゼロから始めるIDAリバースエンジニアリング》学習ノート-18(ライセンスキー作成)

image

main 関数およびそのパラメータ#

第 17 章では、リモートデバッグを通じてプログラムのデバッグを行いました。ここで実験するプログラムは第 17 章のデバッグ後のプログラムから来ています。ここでは著者が提供したプログラムをそのまま使用し、自分でデバッグしたプログラムは読み込まないことにします。

image

この章では、上記のプログラムを分析し、ライセンスキーを作成します。

プログラムを復習し、32 ビットアーキテクチャです。

image

ida を開いてデバッグ後のプログラムを読み込み、まず文字列を確認します。

image

このプログラムを実行すると、コマンドラインにPone un Userと出力され、英語ではEnter a userです。

Pone un Userをクリックすると、プログラムの具体的な位置にジャンプします。

image

「X」キーを押して文字列の参照を確認します。

image

ok をクリックして、参照の具体的な位置に入ります。

image

上の図では、ebp を基準とした関数内で、まずpush ebp命令を実行して前の関数の ebp 値をスタックに保存し、その後mov ebp,espを実行して ebp を関数自体のローカル変数、パラメータ、バッファの基準として設定します。

image

その後、sub esp,94h命令を実行して ebp の基準から 0x94 バイトを取得し、ローカル変数とバッファのために使用します。

関数の任意の変数またはパラメータをダブルクリックすると、ida は関数の静的スタックビューを表示します。下の図で var_4 の表示内容をクリックします。静的スタックビューです。

image

上の図では、この関数には 2 つのパラメータ、すなわち関数のデフォルトパラメータ argc と argv があり、関数呼び出しの前に push 命令を通じてスタックに渡され、戻りアドレス(r)の下に表示されます。(注:書籍ではここで最初にパラメータがないことを説明し、その後 sub_201070 を main 関数にリネームしてからこの 2 つのパラメータが表示されるようになりました)

これらの 2 つのパラメータは表示されていますが、参照されていないため、無引数として扱うこともできます。

image

静的スタックビューに戻ると、S はSTO RED EBPを示し、push ebp命令を通じて前の関数の ebp を保存したものです。その上にはローカル変数スペースがあり、一般的にはスタックを保護するためのvar_4変数があります。バッファオーバーフローを防ぎます。

image

var_4には 2 つの参照があります。最初の参照は関数の起点であり、スタック上に安全なトークンsecurity cookieを保存するために使用されます。

image

これはランダムな数で、関数が実行されると、ebp と排他的論理和(XOR)を取った後、結果がvar_4に保存されます。

image

上の図は別の参照で、var_4の値が ecx に渡され、その後 ebp と排他的論理和(XOR)を取って元の値を復元し、別の関数を呼び出してその値をチェックします。

@__security_check_cookie@4をクリックして、この関数の内部に入ります。以下の図のように。

image

上の図では、値が正常であれば戻りますが、ecx に_security_cookieの値がない場合、プロセスは終了し、戻ることはなく、これはメモリオーバーフローによって var_4 が上書きされたときにのみ発生します。

N キーを押して、var_4COOKIEにリネームします。

image

次の関数をcheck_cookieと名付けます。

image

修正後、最終的には以下のようになります。

image

関数の起点に戻ると、まだ作用が不明な 2 つの変数があります。1 つはvar_90の初期値が0で、もう 1 つはsizeで、初期値は8です。以下の図のように。

image

以下の図では、var_7dの参照を確認します。ある関数が呼び出された後、var_7dの値が AL レジスタに渡され、その後 EDX レジスタに値が渡され、値が 0 であるかどうかを確認し、Good reverser または Bad reverser を出力するかを決定します。したがって、これは 1 バイトの変数で、SUCCESS_FLAG にリネームされ、最終的に計算されたフラグ値です。

image

N キーを押して、var_7dをリネームします。

image

左側の登録成功のコードブロックを緑色に、右側の登録失敗を黄色に変更します。上の図では、JZ 命令を変更するだけで成功登録が実現できます。

ユーザー名とパスワードの処理#

image

上の図では、別の変数var_90があり、初期値は 0 です。プログラムは BUF の各バイトを読み取り、edx に渡し(0x231109 命令の位置で)、var_90と加算します。最初のループでは 0 が加算され、結果がvar_90に保存されます。したがって、その後のループでは edx が以前のすべてのバイトの合計を加算します。var_90SUMMARYにリネームします。以下の図のように。

image

上の図でvar_84はループのカウンタで、カウンタが 1 加算され、最初の 4 バイトのみをループします。var_84が 4 以上になるとループを抜けます。これをCOUNTADDRと名付けます。

image

このループをより良く表示するために、ctrl キーを押しながらこの 3 つのループをクリックし、右クリックしてノードをグループ化します。

image

最終的に以下のように表示されます。

image

グループ化を解除したい場合は、右クリックしてグループを表示しないを選択するか、アイコンをクリックします。

image

ループの後、パスワードを取得します。

image

プログラムは Buf を使用して取得したパスワードを保存します。プログラムはすでにユーザー名の最初の 4 バイトの合計を保存しています。

image

上の図では、プログラムは次にstrlen()関数を呼び出してパスワードの長さを取得します。長さが 4 未満であればプログラムを終了し、4 以上であればプログラムは右側の緑色のコードブロックに入ります。

image

右側の緑色のコードブロックでは、atoi関数を使用してパスワード文字列を 16 進数に変換します。この関数は Python のhex()に似ています。

image

その後、16 進数のパスワードと0x1234を排他的論理和(XOR)演算し、結果を edx 変数に保存します。

image

上の図では、パスワードと0x1234の排他的論理和(XOR)の結果とユーザー名の最初の 4 バイトの合計をsub_231010関数に渡して比較し、比較の結果に基づいて出力結果を決定します。

image

sub_231010関数のパラメータの中で、arg_4 パラメータは最初にスタックに渡されます。これらの 2 つのパラメータをリネームできます。

image

右クリックしてset typeを選択すると、ida はパラメータに基づいて関数のプロトタイプを認識します。

image

関数の宣言は以下の図のようになります。

image

ida は自動的に注釈を付け、渡されたパラメータと一致します。

image

sub_231010内部では、2 つのパラメータを比較する前に、パスワード変数が eax に渡され、shl eax,1が実行され、2 倍に相当します。

image

最後に比較が行われ、これら 2 つの数が等しい場合、プログラムは緑色のコードに移行し、1 を al に渡し、ループを抜け、SUCCESS_FLAGに渡して最終的に登録が成功したかどうかを決定します。

image

アルゴリズムのまとめ#

プログラムは最初にユーザー名の最初の 4 バイトを加算します。
パスワードを 16 進数に変換した後、0x1234 と排他的論理和(XOR)演算を行い、結果を 2 倍にします。

以下にユーザー名に基づく公式を構築します。ライセンスキーもこれに基づいており、ユーザー名に基づいてパスワードを計算します。

x = password (16進数)
(x ^ 0x1234)*2 = SUCCESS_FLAG

したがって、x ^ 0x1234 = (SUCCESS_FLAG/2)

排他的論理和(XOR)演算は可逆であるため、

A ^ B = C
A = B ^ C

したがって、x = (SUCCESS_FLAG/2) ^ 0x1234

Python を使用してライセンスキーを作成#

ユーザーが「pepe」と入力した場合、長さが 8 バイト未満であれば、バイトの合計は以下のように計算できます。

sum = 0
user='pepe'
length=len(user)

for i in range(length):
	sum+=ord(user[i])

print(hex(sum))

したがって、pepe の最初の 4 バイトの合計は次のようになります。

sum = 0
user='pepe'

for i in range(4):
	sum+=ord(user[i])

print(hex(sum))

image

次に、任意の合法的なユーザー名に適用できるライセンスキーを作成します。

sum = 0
user=input("input user name:")

length=len(user)

for i in range(4):
	sum+=ord(user[i])

if(length>=4):
	print(hex(sum))

input () 関数を使用してコマンドライン入力を取得しますが、現在、上記のコードは任意の合法的なアカウントに適用されます。

以前にまとめた公式x = (SUCCESS_FLAG/2) ^ 0x1234に基づいて、ユーザー名の計算結果を 2 で割り、0x1234 と排他的論理和(XOR)演算を行い、16 進数のパスワードを見つけます。

sum = 0
user=input("input user name:")

length=len(user)

for i in range(4):
	sum+=ord(user[i])

print(user)

if(length>=4):
	print("success_flag",hex(sum))
	password = (sum//2)^0x1234
	print("password:",password)

image

現在、ライセンスキーは完成し、パスワードも 16 進数から 10 進数に変換されました。Python のデフォルト出力は 10 進数です。

上記のコードでは、ユーザー名の文字数が 8 文字に達するとプログラムがクラッシュします。なぜなら、文字列の最後には終端文字 null があり、終端文字を含めて 8 を超えてはいけません。もちろん、7 文字の入力は問題ありません。

最後にもう 1 つの問題は、4 文字の合計が奇数である場合です。比較の前にパスワードは 2 倍にされるため、結果は常に偶数になります。このため、この場合は解がありません。

チェックプロセスを追加します。

sum = 0
user=input("input user name:")

length=len(user)

for i in range(4):
	sum+=ord(user[i])

print(sum)
print(user)

if(sum%2==0):
	print("偶数")
	if(length>=4):
		print("success_flag",hex(sum))
		password = (sum//2)^0x1234
		print("password:",password)
else:
	print("奇数")

ユーザー名のバイトの合計 sum を 2 で割った余りをチェックし、0 でない場合、sum は奇数であり、対応するパスワードは解がありません。

奇数の場合

image

偶数の場合

image

これで、ライセンスキーが完成しました。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。