main 関数およびそのパラメータ#
第 17 章では、リモートデバッグを通じてプログラムのデバッグを行いました。ここで実験するプログラムは第 17 章のデバッグ後のプログラムから来ています。ここでは著者が提供したプログラムをそのまま使用し、自分でデバッグしたプログラムは読み込まないことにします。
この章では、上記のプログラムを分析し、ライセンスキーを作成します。
プログラムを復習し、32 ビットアーキテクチャです。
ida を開いてデバッグ後のプログラムを読み込み、まず文字列を確認します。
このプログラムを実行すると、コマンドラインにPone un User
と出力され、英語ではEnter a user
です。
Pone un User
をクリックすると、プログラムの具体的な位置にジャンプします。
「X」キーを押して文字列の参照を確認します。
ok をクリックして、参照の具体的な位置に入ります。
上の図では、ebp を基準とした関数内で、まずpush ebp
命令を実行して前の関数の ebp 値をスタックに保存し、その後mov ebp,esp
を実行して ebp を関数自体のローカル変数、パラメータ、バッファの基準として設定します。
その後、sub esp,94h
命令を実行して ebp の基準から 0x94 バイトを取得し、ローカル変数とバッファのために使用します。
関数の任意の変数またはパラメータをダブルクリックすると、ida は関数の静的スタックビューを表示します。下の図で var_4 の表示内容をクリックします。静的スタックビューです。
上の図では、この関数には 2 つのパラメータ、すなわち関数のデフォルトパラメータ argc と argv があり、関数呼び出しの前に push 命令を通じてスタックに渡され、戻りアドレス(r)の下に表示されます。(注:書籍ではここで最初にパラメータがないことを説明し、その後 sub_201070 を main 関数にリネームしてからこの 2 つのパラメータが表示されるようになりました)
これらの 2 つのパラメータは表示されていますが、参照されていないため、無引数として扱うこともできます。
security cookie などのローカル変数#
静的スタックビューに戻ると、S はSTO RED EBP
を示し、push ebp
命令を通じて前の関数の ebp を保存したものです。その上にはローカル変数スペースがあり、一般的にはスタックを保護するためのvar_4
変数があります。バッファオーバーフローを防ぎます。
var_4
には 2 つの参照があります。最初の参照は関数の起点であり、スタック上に安全なトークンsecurity cookie
を保存するために使用されます。
これはランダムな数で、関数が実行されると、ebp と排他的論理和(XOR)を取った後、結果がvar_4
に保存されます。
上の図は別の参照で、var_4
の値が ecx に渡され、その後 ebp と排他的論理和(XOR)を取って元の値を復元し、別の関数を呼び出してその値をチェックします。
@__security_check_cookie@4
をクリックして、この関数の内部に入ります。以下の図のように。
上の図では、値が正常であれば戻りますが、ecx に_security_cookie
の値がない場合、プロセスは終了し、戻ることはなく、これはメモリオーバーフローによって var_4 が上書きされたときにのみ発生します。
N キーを押して、var_4
をCOOKIE
にリネームします。
次の関数をcheck_cookie
と名付けます。
修正後、最終的には以下のようになります。
関数の起点に戻ると、まだ作用が不明な 2 つの変数があります。1 つはvar_90
の初期値が0
で、もう 1 つはsize
で、初期値は8
です。以下の図のように。
以下の図では、var_7d
の参照を確認します。ある関数が呼び出された後、var_7d
の値が AL レジスタに渡され、その後 EDX レジスタに値が渡され、値が 0 であるかどうかを確認し、Good reverser または Bad reverser を出力するかを決定します。したがって、これは 1 バイトの変数で、SUCCESS_FLAG にリネームされ、最終的に計算されたフラグ値です。
N キーを押して、var_7d
をリネームします。
左側の登録成功のコードブロックを緑色に、右側の登録失敗を黄色に変更します。上の図では、JZ 命令を変更するだけで成功登録が実現できます。
ユーザー名とパスワードの処理#
上の図では、別の変数var_90
があり、初期値は 0 です。プログラムは BUF の各バイトを読み取り、edx に渡し(0x231109 命令の位置で)、var_90
と加算します。最初のループでは 0 が加算され、結果がvar_90
に保存されます。したがって、その後のループでは edx が以前のすべてのバイトの合計を加算します。var_90
をSUMMARY
にリネームします。以下の図のように。
上の図でvar_84
はループのカウンタで、カウンタが 1 加算され、最初の 4 バイトのみをループします。var_84
が 4 以上になるとループを抜けます。これをCOUNTADDR
と名付けます。
このループをより良く表示するために、ctrl キーを押しながらこの 3 つのループをクリックし、右クリックしてノードをグループ化します。
最終的に以下のように表示されます。
グループ化を解除したい場合は、右クリックしてグループを表示しないを選択するか、アイコンをクリックします。
ループの後、パスワードを取得します。
プログラムは Buf を使用して取得したパスワードを保存します。プログラムはすでにユーザー名の最初の 4 バイトの合計を保存しています。
上の図では、プログラムは次にstrlen()
関数を呼び出してパスワードの長さを取得します。長さが 4 未満であればプログラムを終了し、4 以上であればプログラムは右側の緑色のコードブロックに入ります。
右側の緑色のコードブロックでは、atoi
関数を使用してパスワード文字列を 16 進数に変換します。この関数は Python のhex()
に似ています。
その後、16 進数のパスワードと0x1234
を排他的論理和(XOR)演算し、結果を edx 変数に保存します。
上の図では、パスワードと0x1234
の排他的論理和(XOR)の結果とユーザー名の最初の 4 バイトの合計をsub_231010
関数に渡して比較し、比較の結果に基づいて出力結果を決定します。
sub_231010
関数のパラメータの中で、arg_4 パラメータは最初にスタックに渡されます。これらの 2 つのパラメータをリネームできます。
右クリックしてset type
を選択すると、ida はパラメータに基づいて関数のプロトタイプを認識します。
関数の宣言は以下の図のようになります。
ida は自動的に注釈を付け、渡されたパラメータと一致します。
sub_231010
内部では、2 つのパラメータを比較する前に、パスワード変数が eax に渡され、shl eax,1
が実行され、2 倍に相当します。
最後に比較が行われ、これら 2 つの数が等しい場合、プログラムは緑色のコードに移行し、1 を al に渡し、ループを抜け、SUCCESS_FLAG
に渡して最終的に登録が成功したかどうかを決定します。
アルゴリズムのまとめ#
プログラムは最初にユーザー名の最初の 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))
次に、任意の合法的なユーザー名に適用できるライセンスキーを作成します。
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)
現在、ライセンスキーは完成し、パスワードも 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 は奇数であり、対応するパスワードは解がありません。
奇数の場合
偶数の場合
これで、ライセンスキーが完成しました。