ハードコアオールドスクール:QEMUとリバースフロッピーイメージ


明日、7月2日11:00にサンクトペテルブルクで開催されるNeoQUEST-2015 対決の前夜に、オンラインステージの最後の組み立てられていないタスクの記事を公開します!

イベントへの入場は無料であり、情報セキュリティに関心のあるすべての人をお待ちしています! NeoQUESTは、何か新しいことを学び、「ハッカー」スキルを向上させ、同僚とコミュニケーションを取り、最高のハッカーの決定的な競争を見て、素晴らしい時間を過ごすチャンスです!

NeoQUEST-2015レポートの開催地とトピックについては、 こちらをご覧ください

「デザートのために」残されたオンラインステージのタスクは、かなり古いものでした。長い間忘れられていたフロッピーディスクをダンプすることについて話していれば十分でした。 クエスト参加者がどのようにリバースとQEMUをいじらなければならなかったかについて-カットの下で!


タスクの初期データをどうしますか?


タスクでは、task.binファイルが初期データとして機能します。 凡例から判断すると、ブートディスケットのイメージであるはずです。 それをファイルユーティリティに送りましょう。



仮定は真実であることが判明しました。これはディスケットです。 さて、それから起動してみましょう。 仮想マシンとして、QEMUを使用します。 実行する

qemu –fda task.bin 

そして...



...そして何もない。 何らかの理由で、起動に失敗しました-QEMUは「Loading」と書き込み、ハングしました。 最初に、gdbをデバッガとして仮想マシンに接続して、仮想マシン内で何が起こるかを見てみましょう。 gdbを接続するには、仮想マシンの動作モードを知ることが重要です。これは、アプリケーションからデバッガーに送信されるデータの形式に影響するためです。

仮想マシン:内部ビュー


QEMUウィンドウに移動し、Ctrl + Alt + 2を押してコマンドコンソールを開きます。 その中で「情報レジスタ」を実行し、Ctrl + Upの組み合わせで上にスクロールしましょう。



上の図は、注意が必要なフィールドの概要を示しています-CR0およびCSが指すハンドルの属性。 CR0およびCS.ATTRの値から、仮想メモリなしで保護モードが有効になり、32ビットコードが実行されることがわかります。 私たちにとって、これはgdbで次のコマンドでモードを切り替える必要があることを意味します

 set architecture i386 


gdbが32ビットの場合、このアーキテクチャはデフォルトで設定されます。

「–s」オプション(このオプションを使用するとデバッガーを接続できます)を指定してQEMUを実行し、コマンド「target remote localhost:1234」を実行してgdbを有効にします。 EIPに関するいくつかの指示を出力し、仮想マシンがHALTにあり、スタックにゼロがあることを確認します。 ここから来た場所は完全に理解できません。 分解する必要があるようです。



コードの分解とデバッグ


HALTのジャンプが発生する場所を把握して、コードを順番に逆アセンブルしてデバッグします。 フロッピーディスクの最初のセクターから始めましょう。 レガシーモードでフロッピーから起動する場合(そして、それ以外の場合はおそらく動作しません)、BIOSは最初のセクターを読み取り、0x7c00で読み込みます。 ほとんどの場合、最初のセクターのコードのタスクは、ディスクから「継続」をロードし、保護モードに切り替えることです。 ddおよびobjdumpユーティリティを使用して、どんな種類のコードがあるのか​​見てみましょう。



コードを最初から少しスクロールすると、保護モードへの移行を確認できます。 ここでのljmp命令はコードセレクターを変更するために使用され、遷移アドレスは0x7c61です。 分解するときに0x7c00に等しいベースを指定しなかったため、リストではアドレス0x7c61は0x61に対応しています。

これは通常、32ビットコードの実行を開始するために行われます。 さらに、アドレスがgdtrレジスタにあり、値が0x7d95にあり、0x7c4d(リストでは0x4d)にあるlgdtw命令でロードされているgdt構造体を見つけることでこれを確認できます。

gdtでは、オフセット8の記述子のタイプを調べる必要があります。これはljmp命令の最初の引数です。 これは、0x7c61のコードが32ビットであることを意味します。つまり、他のパラメーターを使用してobjdumpを逆アセンブルする必要があります。 task.binからオフセット0x61で対象のコードを選択し、32ビットとして逆アセンブルします。



結果のコードでは、新しい値がセレクターにロードされ、アドレス0x80000へのジャンプが行われます。 仮想マシンを実行し、このアドレスにブレークポイントを設定します。 これを行うには、QEMUは次のコマンドで開始します

 qemu –s –S –fda task.bin 


gdbは以前と同じように接続されます。 ブレークポイントをアドレスに設定します-gdbの「b * 0x80000」、続行-「c」。 ブレークポイントがトリガーされた後、いくつかの指示が表示されます。



最初のjmpを「si」コマンドで実行し、実行するコードを再度出力します。



最初のretの前のコードは分岐が豊富ではなく、何かが発生する可能性がある呼び出しは1つだけです。 0x82961で4Kメモリをダンプして、そこで実行されるコードの種類を見てみましょう。 メモリダンプは、次のコマンドを使用してgdbから取得できます。



結果のダンプコマンドを逆アセンブルします

 objdump –D –b binary –m i386 ./eip_dump.bin > eip.txt 


0x82961の関数には非常に多くの呼び出しが含まれていますが、それ自体は最後に1つのretを持つ連続したコードです。 停止する場所に関心があります。表示されるコードには停止がないため、すべての呼び出しにブレークポイントを設定し、関数の最後に戻ります。

関心のあるアドレスのリストは次のとおりです。0x82970、0x82aef、0x82A5B、0x82A7D、0x82A91、0x82AB0、0x82AEF、0x82B10、0x82B32、0x82B46、0x82B65、0x82C53、0x82CAD。 次に、実行を継続し、各ブレークポイントセットで連続してフォールアウトします。 ブレークポイントに関心があり、その後ブレークが発生します。 retに設定されたブレークポイントであることが判明しました-調査中の関数の最後です。 これは予想外ですが、retの前にプッシュすることに注意を払うと、これは呼び出しポイントへの戻りではなく、新しいコードへの制御の転送であることが明らかになります。 siを実行し、アドレス0x4000020に到達します。



やったー、ついにタスクを開始しました!


思い出すと、halt命令は0x4000260にあり、現在のeipにはるかに近いです。 再度呼び出しを探したり、手でブレークポイントを設定したりしないために、次のようにします-ループ内の1つの命令を実行し、次の命令を出力してeip!= 0x4000260を確認する簡単なスクリプトを記述します。 スクリプトは次のとおりです。

 b *0x4000020 commands 1 while $pc != 0x4000260 x /1i $pc si end x /1i $pc end c 


script.txtファイルにスクリプトを配置し、sourceコマンドを使用してgdbで実行します。 実行後、次の結果が得られます。



コード内で2つのcpuid呼び出しが印象的で、その後ハングが発生します。 これらはある種のチェックのようです。 彼らがチェックするものを見てみましょう。 最初の呼び出しは、パラメーターeax = 0x80000000で行われます。その結果、eaxには、cpuid命令に渡すことができるパラメーターの最大値が含まれます。 次に、値が0x80000001と比較されます。これは、次の呼び出しを行う可能性のテストです。 2番目の呼び出しはパラメーターeax = 0x800000001を使用して行われ、edxの29番目のビットがチェックされます。これは、ロングモードがサポートされている場合は1に設定されます。

実行しているQEMUがロングモードをサポートしていないため、仮想マシンがフリーズしているようです。 次のように仮想マシンを起動します。



やったー、なんとかタスクを実行できた! 残っている唯一のことはそれを実現することです! 一般に、作業中のLinuxが64ビットであれば、上記のフリーズの問題は発生しませんでした。 この場合、システムの処理能力に不運がありました。

パスワード推測


タスク自体に到達すると、何をする必要があるかが明らかになります。 どうやら、検証アルゴリズムを満たす「パスワード」を選択する必要があります。 これを行うには、パスワードが検証される場所を見つけます。

パスワード検証にできる限り近いところで、仮想マシンの実行を停止しようとします。 エラーメッセージが表示される前に、入力したパスワードが4行目に印刷されることに気付くかもしれません。 ほとんどの場合、この時点でチェックはまだ完了していないため、この場所でデバッガから抜け出すと、すでにテストに合格していないパスワードがすでに入力されていることがわかります。 画面にパスワードが表示され、その検証が順番に呼び出される機能にスタックを移動するために残ります。



適切な場所に到達するために、コードのどこにブレークポイントを置くかを決定する方法は? 画面に文字を印刷するには、次の2つの方法があります。

  1. 簡単な方法は、テキストモードで0xb8000のビデオメモリに文字を書き込むことです。これは、起動時にデフォルトで有効になっています。
  2. 難しい方法は、ビデオカードを構成し、画面上にポイントを描画し、フォントを使用してポイントにシンボルを描画する機能を提供するドライバーを記述することです。 ここで行われているように、ドライバーの代わりにVBE BIOSを使用できます。


簡単な方法が使用されたとします。 次に、ビデオメモリ、つまり4行目の最初の文字にアクセスするためのブレークポイントを設定できます。 ビデオメモリはアドレス0xb8000で始まり、文字列サイズは80文字で、各文字に2バイト(文字+色)が割り当てられ、目的のアドレスは0xb8000 + 80 * 2 * 3 = 0xb81e0です。 gdbのメモリに書き込むようにブレークポイントを設定するコマンドは次のようになります。

 watch *0xb81e0 




仮定は真実で、キャラクターをメモリに書き込んだ直後に脱落しました。 ブレークポイントは不要になりました。削除できます。 もう1つ仮定してみましょう-印刷とパスワードの確認コードが順番に呼び出される関数があるとしましょう。 ソースコードは次のようになります。



私たちの目標は、CheckPass()関数を見つけることです。 これを行うには、PrintPass()に埋め込まれた関数からの戻りアドレスにブレークポイントを設定し、実行を継続します。 新しくインストールしたブレークポイントから脱落し、「パスワードが正しくありません」というメッセージがまだ印刷されていない場合は、新しいものを入れて続行します。

印刷した場合、最後から2番目のセットは必要なものです。タスク()の本体でPrintPass()を呼び出した直後にそれが立っていました。 返信先住所を取得する方法を見つけます。 コードが特定のフラグなしでコンパイルされた場合、関数の先頭に「プッシュ$ rbp; mov $ rsp、$ rbp”新しいスタックフレームが形成されます。 この場合、戻りアドレスは$ rbp + 8に保存されます。 これは簡単に確認できます。



実際、アドレス0xfffff8000020e5b5の前にcallステートメントがあります。 これで計画を実行できます。

スクリプトを書く


スタックの深さがわからないため、画面に「パスワードが正しくありません」と表示されるまでQEMUがスタックを起動するgdbの小さなスクリプトを作成します。

 set confirm off # save start values of first 4 chars from 5th row of screen set $start_vmem_val = *(unsigned long long*)(0xb8280) set $curr_vmem_val = $start_vmem_val # if nothing changed in 5th row of screen, we continue while $start_vmem_val == $curr_vmem_val # delete all old breakpoints d # get return addres from stack and set breakpoint on it. Then, continue. set $ret_addr = *(unsigned long long*)($rbp + 8) b *$ret_addr c set $curr_vmem_val = *(unsigned long long*)(0xb8280) end 


以前に行ったように、スクリプトをファイルに保存し、ソースコマンドを実行します。 次のものが得られます。



スクリプトは、0x20069cのブレークポイントに到達せずにクラッシュしましたが、「パスワードが正しくありません」というメッセージが出力されました。 これは、task()と呼ばれる関数があるという仮定が正しいことを意味します。 ハングは、「パスワードが正しくありません」というメッセージが画面に出力された後にタスク()関数が戻らないことを示します。 ただし、これは重要ではありません。主なことは、最後から13番目のブレークポイントを設定するPrintPass()関数からの戻りアドレスを知っていることです。

検索が続行されます...


受信したばかりのアドレス0xfffff80000205808からパスワードを確認する手順の検索を続行します。QEMUを実行し、このアドレスにブレークポイントを設定して、任意のパスワードを入力します。 RIPから数バイト戻って、先ほど残した関数のアドレスを見つけることで、コードダンプを削除します。



コマンド「objdump –D –b binary –m i386:x86-64 –adjust-vma = 0xfffff800002057fc task.bin> task.txt」を使用して、結果のダンプを逆アセンブルします。

0xfffff80000203358で関数を終了しただけであり、このアドレスは受信したダンプで数回検出されることに注意してください。

fffff800002057fc <.data>:
fffff800002057e3:48 8d 85 60 ff ff ff lea -0xa0(%rbp)、%rax
fffff800002057ea:48 89 c6 mov%rax、%rsi
fffff800002057ed:48 bf 56 15 21 00 00 movabs $ 0xfffff80000211556、%rdi
fffff800002057f4:f8 ff ff
fffff800002057f7:b8 00 00 00 00 mov $ 0x0、%eax
fffff800002057fc:48 ba 58 33 20 00 00 movabs $ 0xfffff80000203358、%rdx
fffff80000205803:f8 ff ff
fffff80000205806:ff d2 callq *%rdx
rip => fffff80000205808:48 b8 08 15 21 00 00 movabs $ 0xfffff80000211508、%rax
fffff8000020580f:f8 ff ff
...
fffff80000205930:48 bf 65 15 21 00 00 movabs $ 0xfffff80000211565、%rdi
fffff80000205937:f8 ff ff
fffff8000020593a:b8 00 00 00 00 mov $ 0x0、%eax
fffff8000020593f:48 ba 58 33 20 00 00 movabs $ 0xfffff80000203358、%rdx
fffff80000205946:f8 ff ff
fffff80000205949:ff d2 callq *%rdx
...
fffff8000020594d:48 bf 78 15 21 00 00 movabs $ 0xfffff80000211578、%rdi
fffff80000205954:f8 ff ff
fffff80000205957:b8 00 00 00 00 mov $ 0x0、%eax
fffff8000020595c:48 ba 58 33 20 00 00 movabs $ 0xfffff80000203358、%rdx
fffff80000205963:f8 ff ff
fffff80000205966:ff d2 callq *%rdx

考慮されるコードは64ビットであり、64ビットコードで使用される2つの主な呼び出し規則があります。
  1. Microsoft x64呼び出し規約
  2. System V ABI

この場合、呼び出しの引数はRDI、RSI、RDXレジスタなどを介して渡されるため、System Vが使用されます。 最低限、テキストを表示する関数を残しました。この関数は数回呼び出されます。 引数0xfffff80000211556と-0xa0(%rbp)で最初に呼び出され、2回目は0xfffff80000211565で、3回目は0xfffff80000211578で呼び出されます。 これらのアドレスにあるものを見てみましょう。



関数0xfffff80000203358はprintfであり、チェックの結果に応じて、異なるメッセージを表示します。 文字列「123」は入力されたパスワードです。 表示されるメッセージに応じて見てみましょう。

fffff8000020591c:movabs $ 0xfffff800002114c0、%rax
fffff80000205926:mov 0x38(%rax)、%rax
fffff8000020592a:cmp $ 0x1、%rax if(g_struct.res == 1)
、==== <fffff8000020592e:jne 0xfffff8000020594d {
| fffff80000205930:movabs $ 0xfffff80000211565、%rdi
| fffff8000020593a:mov $ 0x0、%eax
| fffff8000020593f:movabs $ 0xfffff80000203358、%rdx
| fffff80000205949:callq *%rdx printf(「正しいパスワード!」);
| 、== <fffff8000020594b:jmp 0xfffff80000205968}
`====> fffff8000020594d:movabs $ 0xfffff80000211578、%rdi else
| fffff80000205957:mov $ 0x0、%eax {
| fffff8000020595c:movabs $ 0xfffff80000203358、%rdx
| fffff80000205966:callq *%rdx printf(「パスワードが正しくありません。」);
`==> fffff80000205968:movabs $ 0xfffff80000204b83、%rax}
fffff80000205972:callq *%rax some_func();
fffff80000205974:leaveq
fffff80000205975:retq

検証結果は、構造体のアドレス0xfffff800002114c0にオフセット0x38で保存されます。 検討中の関数にこの構造体への呼び出しがあるかどうかを見てみましょう。

fffff8000020587c:mov $ 0x48、%edx
fffff80000205881:mov $ 0x0、%esi
fffff80000205886:movabs $ 0xfffff800002114c0、%rdi
fffff80000205890:movabs $ 0xfffff80000203d40、%rax
fffff8000020589a:callq *%rax memset(&g_struct、0、0x48);
fffff8000020589c:lea -0xa0(%rbp)、%rdx
fffff800002058a3:movabs $ 0xfffff800002114c0、%rax
fffff800002058ad:mov%rdx、(%rax)*(u64 *)&g_struct = password;
fffff800002058b0:lea -0xa0(%rbp)、%rdx
fffff800002058b7:movabs $ 0xfffff800002114c0、%rax
fffff800002058c1:mov%rdx、0x20(%rax)*((u64 *)&g_struct + 4)= password;

コードの上には、3つの引数を持つ関数呼び出しがあり、そのうちの1つは構造体へのポインターです。 この関数のコードに目を向けると、これがmemsetであることが明らかになります。 入力されたパスワードを含む行へのポインターは、オフセット0および32(0x20)で構造体に2回書き込まれます。 どうやらこれは初期化です。 初期化から結果の確認までのコードを見ると、次のことがわかります。

; 上記はg_struct構造体の初期化です
fffff800002058c5:movzbl -0x1(%rbp)、%eax l_var1 = -0x1(%rbp);
fffff800002058c9:mov%rax、%rdi
fffff800002058cc:movabs $ 0xfffff80000203e94、%rax
fffff800002058d6:callq *%rax if(func1(l_var1))
fffff800002058d8:test%rax、%rax {
fffff800002058db:sete%al
fffff800002058de:test%al、%al
、==== <fffff800002058e0:je 0xfffff80000205909
| fffff800002058e2:movabs $ 0xfffff80000211508、%rax asm(
| fffff800002058ec:mov(%rax)、%rax push * 0xfffff80000211508
| fffff800002058ef:mov%rax、%rdx retq
| fffff800002058f2:push%rdx);
| fffff800002058f3:retq
| fffff800002058f4:movzbl -0x1(%rbp)、%eax
| fffff800002058f8:mov%rax、%rdi
| fffff800002058fb:movabs $ 0xfffff800002040b2、%rax
| fffff80000205905:callq *%rax func2(l_var1);
| 、== <fffff80000205907:jmp fffff8000020591c}
`====> fffff80000205909:movzbl -0x1(%rbp)、%eax else
| fffff8000020590d:mov%rax、%rdi {
| fffff80000205910:movabs $ 0xfffff800002040b2、%rax
| fffff8000020591a:callq *%rax func2(l_var1);
`==> fffff8000020591c:movabs $ 0xfffff800002114c0、%rax}
; 以下はチェックと出力です

パスワード確認コードを含む可能性のあるコード内のブランチは、黄色で強調表示されます。 コードの中央のpush / retコンストラクトはやや奇妙に見えます。これは、その後の実行がどのように続くかが明確ではないためです。 私たちはまだパスワード確認機能を探しています。

アドレス0xfffff800002040b2および0xfffff80000203e94の関数は、入力されたパスワードを使用せず、見つかった構造にアクセスしません。 興味深いのは、アドレス0xfffff80000600000へのジャンプが行われるプッシュ、retq命令のカップルですが、このアドレスにどのようなコードがあるかを確認しようとすると、次のようになります。



実行しようとすると、アドレス0xfffff80000209ac5に移動します。 なぜこれが起こっているのですか? メモリアクセスエラーメッセージは、このアドレスで仮想メモリが使用できないことを示唆しています。 これは、QEMUコンソールで「info mem」を実行することで確認できます。



実際、アドレス0xf80000600000からの2メガバイトの範囲はマップされていません。 64ビットモードで仮想アドレスを変換する場合、上位4桁がゼロであり、fではないことに注意してください。上位16ビットは使用されず、アドレス0x0はアドレス0xfffff00000000000と等しくなります。 凍結されていないアドレスにアクセスすると、#PF(ページフォールト)が発生し、問題のアドレスがCR2に書き込まれ、対応する例外ハンドラーに制御が転送されます。 QEMUコンソールでCR2レジスタの値を見ると、この仮定の精度をもう一度確認できます-0xfffff80000600000に等しいです。

コードを注意深く見る


割り込みハンドラーでは、状態は最初に保存され、最初のCコードは0xfffff8000020da3cに表示されます。 興味深い場所があります:
...
0xfffff8000020da5b:cmp $ 0xe、%rax
0xfffff8000020da5f:jne 0xfffff8000020da95
0xfffff8000020da61:mov -0x18(%rbp)、%rax
0xfffff8000020da65:mov 0xb8(%rax)、%rdx
0xfffff8000020da6c:movabs $ 0xfffff80000211508、%rax
0xfffff8000020da76:mov(%rax)、%rax
0xfffff8000020da79:cmp%rax、%rdx
0xfffff8000020da7c:jb 0xfffff8000020da95
0xfffff8000020da7e:mov -0x18(%rbp)、%rax
0xfffff8000020da82:mov%rax、%rdi
0xfffff8000020da85:movabs $ 0xfffff80000204df8、%rax
0xfffff8000020da8f:callq *%rax
...
0xe( #PF )との比較は例外の原因の確認に非常に似ており、0xfffff80000211508で値0xfffff80000600000が検出され、さらに1つの比較が行われます。 両方の条件が満たされると、アドレス0xfffff80000204df8への呼び出しが発生します。 そこで、次のコードを見ることができます。
...
0xfffff80000204e19:movabs $ 0xfffff8000020fda0、%rax
0xfffff80000204e23:lea(%rdx、%rax、1)、%rax
0xfffff80000204e27:mov(%rax)、%rdx
0xfffff80000204e2a:mov%rdx、-0x50(%rbp)
0xfffff80000204e2e:mov 0x8(%rax)、%rdx
0xfffff80000204e32:mov%rdx、-0x48(%rbp)
0xfffff80000204e36:mov 0x10(%rax)、%rax
0xfffff80000204e3a:mov%rax、-0x40(%rbp)
0xfffff80000204e3e:mov -0x50(%rbp)、%rax
0xfffff80000204e42:cmp $ 0x726574、%rax
0xfffff80000204e48:je 0xfffff80000205418
0xfffff80000204e4e:cmp $ 0x726574、%rax
0xfffff80000204e54:ja 0xfffff80000204ea4
0xfffff80000204e56:cmp $ 0x69667a、%rax
0xfffff80000204e5c:je 0xfffff80000205067
0xfffff80000204e62:cmp $ 0x69667a、%rax
0xfffff80000204e68:ja 0xfffff80000204e87
0xfffff80000204e6a:cmp $ 0x616464、%rax
0xfffff80000204e70:je 0xfffff80000205225
...
関数の大部分は、いくつかのcmp / je命令で占められています。その豊富さは、Cコードに長いスイッチ/ケースがあり、各値に独自のハンドラーがあることを示唆しています。 アドレス0xfffff8000020fda0で読み取られた8バイトの値は、オフセットを使用して比較として機能します。

このコードにブレークポイントを設定すると、ブレークポイントが複数回実行され、オフセットが常に24の倍数であることがわかります。 命令の長さが24バイトの仮想マシンのように見えます。最初の8バイトは命令のシグネチャで、残りの16バイトはパラメーターです。 「dump memory vmcode.bin 0xfffff8000020fda0 0xfffff80000210da0」コマンドを使用して、0xfffff8000020fda0でメモリダンプを作成し、16進エディターで開きます(手元にOktetaがありました)。



図の右側は、命令の署名がASCII文字の組み合わせの形式でエンコードされていることを明確に示しています。 指示の中には、llac、tixe、busなどがあり、逆さまに見えることを示唆しています。 これは、Cコードでは、単一引用符で囲まれた値として書き込まれ、リトルエンディアンで保存されているためです。 命令のパラメーターは、値r0、r1、...など、または数値です。

仮想マシンのコードにアクセスしてください!


原則として、仮想マシンのコードに到達した後、それを分解することは技術的な問題です。 命令シグニチャーは、それらがどのように機能するかを明確に示しており、何かをさらに明確にする必要がある場合は、0xfffff80000204df8の関数で対応するハンドラーを見つけるだけです。 擬似コードでは、VMコードは次のようになります。

 r3 = 5381; while (1) { r1 = *(u8*)r0; if (r1 == 0) break; r3 = r3 * 33 + r1; } // check DJB hash if (r3 != 0x40e1baa8ff648029) { return 0; } r5 << 64 + r3 = (u128)hexstr2val(r4) if (r5 - r3 != 0x2a60386296a57940) { return 0; } r2 = r3 >> 32; r1 = (r3 << 32) >> 32; if (r2 - r1 != 0x3394749a) { return 0; } r2 = (r3 << 32) >> 48; r1 = (r3 << 48) >> 48; if (r2 - r1 != 0x465e) { return 0; } return 1; // success 


入力された行からDJBハッシュを確認した後、キーが満たさなければならない追加の条件があります。

キー選択プログラムの例


キーはチェックに明示的には存在しませんが、擬似コードの4つの条件に基づいて、迅速に選択できます。以下は、正しいキーを選択するプログラムの例です。

 int main(int argc, char **argv) { unsigned long long d64 = 0x2a60386296a57940; unsigned long long d32 = 0x3394749a; unsigned long long d16 = 0x465e; unsigned long long step = 0x1000100010001; unsigned long long key_l = ((d32 << 32) + (d16 << 48)) + ((d16 << 16) + 0); unsigned long long key_h = key_l + d64; std::stringstream key_ss; unsigned int i = 0; while (1) { key_ss << std::hex << key_h; key_ss << std::hex << std::setw(16) << std::setfill('0') << key_l; unsigned long long key_str_hash = djb2_hash(key_ss.str().c_str()); if (key_str_hash == 0x40e1baa8ff648029) { std::cout << "Success! " << i << "\n"; std::cout << "res_key = '" << key_ss.str() << "'\n"; } key_h += step; key_l += step; i++; key_ss.str(""); } return 0; } 


楽しみは終わりました!パスワード検証プロセスで例外がどのように使用されるかをさらに明確にするために、一般的なコールフロー図を以下に示します。



VM命令の実行サイクルは#PF生成で始まり、例外ハンドラーで1つのVM命令が実行されます。この命令が終了命令である場合、longjmpが実行され、最初の#PFが実行される前に戻ります。次に、鍵の検証結果が印刷されます。他のVM命令が実行された場合、例外ハンドラーは#PFが発生する前に状態を復元し、サイクルが再び開始されます。

ハブロフスク市民への質問


小さな余談:結果のダンプを分析するには、IDAに似た逆アセンブラーを使用すると非常に便利です。これは、メモリダンプを(ダンプ、予備など)にロードし、コード、データ、関数および変数に名前を付ける場所を示すことができるためです。ただし、IDAのデモおよび試用版は64ビットコードでは機能しません。

私はこの目的のために必要なすべてを行うことができる無料のレーダー2を適応させようとしましたが、rasmは「movabs $ 0xfffff800002114c0、%rax」のような命令を正しく解析しないという事実に遭遇し、一部のバージョンではコードがロードされるアドレスを設定できません(bin.laddr)、4Gbより上。 Habrの読者の1人が、radere2でこのようなダンプをロードして明確に分析する方法を説明してくれたら、感謝します。ジョブの解析では、objdump、gdb、および$ EDITORに限定されていました。

NeoQUEST 2015参加者に何を期待しますか?


参加者は8時間のフリーメーソンの伝説と7つのタスクを続けることが期待されています(参加者の競争の開始は10:00です)!課題は情報セキュリティのさまざまな側面に関連するため、既存のスキルに従って、誰もが自分の好みに合った課題を見つけることができます。18:00に要約し、勝者がメイン賞を受け取ります。国際会議の1つへの旅行、「シルバー」と「ゴールド」の参加者もクールな賞品を獲得します。サンクトペテルブルクの主要なサイバー安全イベントの前には何も残っていません!

Source: https://habr.com/ru/post/J261081/


All Articles