アセンブリの基礎#
習得度:アセンブリを理解し、ガジェットを分析し、ステップデバッグでレジスタの状態を理解する
参考記事:x86_64 アセンブリの一つ:AT&T アセンブリ構文_x86_64 アセンブリ at&t-CSDN ブログ
レジスタ#
一般的な x86 CPU
レジスタは 8 つあります:EAX
、EBX
、ECX
、EDX
、EDI
、ESI
、EBP
、ESP
CPU はまずレジスタを読み書きし、その後レジスタ、キャッシュ、メモリを通じてデータを交換し、バッファリングの目的を達成します。レジスタには名前を使ってアクセスできるため、アクセス速度が最も速く、ゼロレベルキャッシュとも呼ばれます。
アクセス速度は高い順に次のようになります: レジスタ > 1次キャッシュ > 2次キャッシュ > 3次キャッシュ > メモリ > ハードディスク
一般レジスタとその用途#
上記の 8 つのレジスタにはそれぞれ特定の用途があります。ここでは32ビット CPU
を例にとって、これらのレジスタの役割を簡単に説明します。以下の表に整理しました:
レジスタ | 意味 | 用途 | 含まれるレジスタ |
---|---|---|---|
EAX | 累積 (Accumulator) レジスタ | 主に乗算、除算および関数の戻り値に使用される | AX(AH、AL) |
EBX | 基底 (Base) レジスタ | 主にメモリデータのポインタとして使用される、または基底としてメモリにアクセスするために使用される | BX(BH、BL) |
ECX | カウンタ (Counter) レジスタ | 主に文字列操作やループ操作のカウンタとして使用される | CX(CH、CL) |
EDX | データ (Data) レジスタ | 主に乗算、除算および I/O ポインタに使用される | DX(DH、DL) |
ESI | ソースインデックス (Source Index) レジスタ | 主にメモリデータポインタおよびソース文字列ポインタとして使用される | SI |
EDI | デスティネーションインデックス (Destination Index) レジスタ | 主にメモリデータポインタおよびデスティネーション文字列ポインタとして使用される | DI |
ESP | スタックポインタ (Stack Point) レジスタ | スタックのスタックトップポインタとしてのみ使用される;算術演算やデータ転送には使用できない | SP |
EBP | 基底ポインタ (Base Point) レジスタ | スタックポインタとしてのみ使用され、スタック内の任意のアドレスにアクセスできる。ESP 内のデータを中継するためによく使用され、スタックにアクセスするための基底としても使用される;算術演算やデータ転送には使用できない | BP |
上記の表の各一般レジスタの後には他の名前もあります。rax、eax、ax、ah、al は実際には同じレジスタを示しており、異なる範囲を含んでいるだけです。
以下は 64 ビットレジスタの対照関係です:
|63..32|31..16|15-8|7-0|
|AH. |AL.|
|AX......|
|EAX............|
|RAX...................|
命令ポインタレジスタ#
命令ポインタレジスタ(RIP
)は、次に実行される命令の論理アドレスを含みます。
通常、命令を 1 つ取り出すたびに、RIP は自動的に次の命令を指すように増加します。x86_64 では RIP の増加は 8 バイトのオフセットです。
しかし、RIP は常に増加するわけではなく、例外もあります。例えばcall
命令とret
命令です。call
命令は現在の RIP の内容をスタックにプッシュし、プログラムの実行権をターゲット関数に渡します。ret
命令はスタックから出力操作を実行し、以前にスタックにプッシュされた 8 バイトの RIP アドレスをポップして、再び RIP に格納します。
フラグレジスタ(EFLAGS)#
アセンブリ言語命令#
一般的なアセンブリ命令:mov
、je
、jmp
、call
、add
、sub
、inc
、dec
、and
、or
データ転送命令#
命令 | 名称 | 例 | 備考 |
---|---|---|---|
MOV | 転送命令 | MOV dest, src | データを src から dest に移動 |
PUSH | プッシュ命令 | PUSH src | ソースオペランド src をスタックにプッシュ |
POP | ポップ命令 | POP dest | スタックのトップからデータを dest にポップ |
算術演算命令#
命令 | 名称 | 例 | 備考 |
---|---|---|---|
ADD | 加算命令 | ADD dest, src | dest に src を加算 |
SUB | 減算命令 | SUB dest, src | dest から src を減算 |
INC | 加 1 命令 | INC dest | dest に 1 を加算 |
DEC | 減 1 命令 | DEC dest | dest から 1 を減算 |
論理演算命令#
命令 | 名称 | 例 | 備考 |
---|---|---|---|
NOT | 反転命令 | NOT dest | オペランド dest をビット単位で反転 |
AND | 論理積命令 | AND dest, src | dest と src を論理積演算し、結果を dest に格納 |
OR | 論理和命令 | OR dest, src | dest と src を論理和演算し、結果を dest に格納 |
XOR | 排他的論理和命令 | XOR dest, src | dest と src を排他的論理和演算し、結果を dest に格納 |
ループ制御命令#
命令 | 名称 | 例 | 備考 |
---|---|---|---|
LOOP | カウントループ命令 | LOOP label | ECX の値を 1 減らし、ECX の値が 0 でない場合は label にジャンプし、そうでなければ LOOP の後の文を実行 |
転送命令#
命令 | 名称 | 例 | 備考 |
---|---|---|---|
JMP | 無条件転送命令 | JMP label | 無条件に label の位置に転送 |
CALL | プロシージャ呼び出し命令 | CALL label | label を直接呼び出す |
JE | 条件転送命令 | JE label | zf =1 の場合、label の位置にジャンプ |
JNE | 条件転送命令 | JNE label | zf=0 の場合、label の位置にジャンプ |
Linux と Windows のアセンブリの違い#
linux
と windows
のアセンブリ構文は異なります。実際、2 つの構文の違いはシステムの違いとは絶対的な関係はありません。一般的に、linux
では gcc/g++
コンパイラを使用し、windows
ではマイクロソフトの cl
すなわち MSBUILD
を使用します。そのため、異なるコードが生成されるのはコンパイラが異なるためであり、gcc
では AT&T のアセンブリ構文形式が採用され、MSBUILD
では Intel アセンブリ構文形式が採用されています。
差異 | Intel | AT&T |
---|---|---|
レジスタ名の参照 | eax | %eax |
代入オペランドの順序 | mov dest, src | movl src, dest |
レジスタ、即値命令の前接頭辞 | mov ebx, 0xd00d | movl $0xd00d, %ebx |
レジスタ間接アドレス指定 | [eax] | (%eax) |
データ型サイズ | オペコードの後に接尾辞文字を追加、「l」 32 ビット、「w」 16 ビット、「b」 8 ビット(mov dx, word ptr [eax]) | オペランドの前に dword ptr、word ptr、byte ptr の形式を追加(movb % bl % al) |
アドレッシング方式#
直接アドレッシング
メモリアドレッシング:[ ]
オーバーフロー(符号付き & 上下オーバーフロー)#
- ストレージビット数が不足
- 符号ビットへのオーバーフロー
整数オーバーフローは他の脆弱性と組み合わせて使用されます。
個人的には、符号ビット数の進位はオーバーフローを示すと考えています。
LINUX ファイルの基礎#
保護レベル:0-3
0 - カーネル
3 - ユーザー
仮想メモリ:物理メモリが MMU によって変換されたアドレス。システムは各ユーザープロセスに仮想メモリ空間を割り当てます。
ビッグエンディアンとリトルエンディアン#
ビッグエンディアン:データの高位 -> コンピュータアドレスの低位(人間の読みやすい習慣により適合)
リトルエンディアン:データの低位 -> コンピュータアドレスの低位(直感に反するが、ストレージロジック、演算規則により適合)
コンピュータは文字列を低アドレスから高アドレスに出力します。
Linux のデータストレージ形式はリトルエンディアンであり、ARM アーキテクチャはビッグエンディアンです。
文字列形式で数字を入力する際は、形式に注意が必要です。Linux は低から高にデータを読み取ります。pwntools を使用して変換できます。
ファイルディスクリプタ#
各ファイルディスクリプタは 1 つのオープンファイルに対応しています。
- 0
- 1
- 2
stdin->buf->stdout
例えば:
read(0,buf,size)
write(1,buf,size)
スタック(stack)#
制限された配列、片側のみ操作可能
データ構造:後入れ先出し(LIFO)、関数呼び出しの順序と同じ
関数の開始実行:main->funA->funB
関数の完了順序:funB->funA->main
基本操作:push でプッシュ、pop でポップ
関数呼び出し命令 call、戻り命令 ret
オペレーティングシステムは各プログラムにスタックを設定し、プログラムの各独立した関数には独立したスタックフレームがあります。
Linux のスタックは高アドレス(スタックトップ)から低アドレス(スタックボトム)に成長します。
多くのアルゴリズム(DFS など)はスタックを利用し、再帰的に実装されます。
呼び出し規約 (Calling Convention)#
呼び出し規約とは#
関数の呼び出しプロセスには 2 つの参加者がいます。1 つは呼び出し側 caller、もう 1 つは呼び出される側 callee。
呼び出し規約は、caller と callee が関数呼び出しを実現するためにどのように協力するかを規定します。具体的には以下の内容が含まれます:
- 関数の引数はどこに格納されるか。レジスタに格納されるのか?それともスタックに格納されるのか?どのレジスタに格納されるのか?スタックのどの位置に格納されるのか?
- 関数の引数はどの順序で渡されるか。左から右に引数をスタックにプッシュするのか、それとも右から左にプッシュするのか?
- 戻り値はどのように caller に渡されるか。レジスタに格納されるのか、それとも他の場所に格納されるのか?
- などなど
では、なぜ呼び出し規約が必要なのでしょうか?
例えば、アセンブリ言語でコードを書く際に統一された規範がなければ、A
は引数をスタックに置くことを好み、B
はレジスタに置くことを好み、C
は…、それぞれが自分の考えに従ってコードを書くことになります。こうなると、A
が他の人のコードを呼び出そうとすると、他の人の習慣に従わなければならず、例えばB
を呼び出す場合、A
は B が定めたレジスタに引数を置かなければなりません。C
を呼び出す場合は、また別の方法になります…
呼び出し規約は上記の問題を解決するために存在し、関数呼び出しの詳細を規定します。これにより、全員が同じ規約に従い、他の人が書いたコードを呼び出す際に何も変更する必要がなくなります。
関数呼び出しスタック#
- 関数呼び出し:関数が呼び出されると、プログラムは呼び出しスタック上に新しいスタックフレームを割り当てます。スタックフレームには関数の引数、局所変数、戻りアドレスなどの情報が含まれます。
- 引数の渡し:関数呼び出しプロセスでは、引数がスタック操作を通じて呼び出される関数に渡されます。これらの引数はスタックフレームに格納され、関数内部で使用されます。
- 関数の実行:呼び出された関数が実行を開始し、スタックフレーム内の引数と局所変数を使用します。関数の実行プロセスは複雑なロジックや計算を含むことがあります。
- 戻り値の処理:関数の実行が完了すると、プログラムはその関数を呼び出したコードの位置に戻ります。この位置はスタックフレーム内の戻りアドレスによって指定されます。関数に戻り値がある場合、その値は呼び出し側のスタックフレームにプッシュされます。
- スタックフレームの破棄:関数呼び出しが完了すると、そのスタックフレームは呼び出しスタックからポップされて破棄され、占有していたメモリリソースが解放されます。
具体的な関数呼び出しの流れ#
- pop
pop rax の作用:
mov rax [rsp];
// スタックトップのデータをレジスタにポップ
add rsp 8;
// スタックトップポインタを 1 単位下げる
- push
push rax の作用:
sub rsp 8;
// スタックフレームを 1 単位上げる
mov [rsp] rax;
// レジスタの値をスタックトップに格納
- jmp
即時ジャンプ、関数呼び出しを含まず、ループや if-else に使用されます。
call 1234h の作用:
mov rip 1234h;
- call
関数呼び出し、戻りアドレスを保存する必要があります。
call 1234h の作用:
push rip;
mov rip 1234h;
- ret
pop rip
例:main が funB を呼び出し、funB が funA を呼び出す。スタックフレームの変化を段階的に分析します:
関数呼び出しプロセス中:
- 関数を呼び出す:
rip
をスタックにプッシュし、戻りアドレスとして保存します。(call)
- 呼び出された関数:
rbp
をスタックにプッシュし、現在のスタックフレームの基準点として使用します。rsp
の値をrbp
に割り当て、rbp
が現在のスタックフレームの底を指すようにします。- 局所変数や一時データのためにスタックスペースを割り当て、
rsp
を適切なサイズだけ減らします。 rsp
を基準ポインタとして使用して関数の引数や局所変数にアクセスします。
関数が戻るとき:leave;ret;
- 呼び出された関数:
- スタックに割り当てられた局所変数や一時データをポップします。
rsp
を関数呼び出し時の値に戻します。
- 呼び出し側の関数:
- スタックから戻りアドレスをポップします。
rip
を戻りアドレスに更新します。
スタックフレームの変化の示意図:
+----------------------------+
| main 関数スタックフレーム |
+----------------------------+
| 戻りアドレス |
| rbp (main 関数の基準ポインタ) |
+----------------------------+
| funB すなわち呼び出し関数スタックフレーム |
+----------------------------+
| 戻りアドレス |
| rbp (funB 関数の基準ポインタ) |
+----------------------------+
| funA すなわち呼び出された関数スタックフレーム |
+----------------------------+
| rbp (funA 関数の基準ポインタ) |
| 局所変数 |
+----------------------------+
引数の渡し方#
関数の戻り値は RAX に渡されます。
x86-64 関数の呼び出し規約は次のようになります:
-
左から右に引数を順次
RDI
,RSI
,RDX
,RCX
,R8
,R9
に渡します。 -
関数の引数が 6 個を超える場合、右から左にスタックにプッシュして渡します。
システムコール#
syscall 命令#
システム関数を呼び出すために使用され、呼び出す際にはシステムコール番号を指定します(64 ビット Linux システムコール表をオンラインで確認できます)。
システムコール番号は RAX レジスタに存在し、パラメータを設定した後、syscall を実行します。
例:read (0,buf,size) を呼び出す
mov rax 0;
mov rdi 0;
mov rsi buf;
mov rdx size;
syscall;
ELF ファイル構造#
ELF ファイル形式#
ELF (Executable and Linkable Format) は Linux におけるバイナリ実行可能ファイル形式です。
ELF ファイルヘッダー(ELF Header)#
readelf -h コマンドを使用して ELF ファイルのファイルヘッダーを読み取ることができます。ELF ヘッダーにはプログラムのエントリポイント(Entry Point Address)、セグメント情報およびセクション情報が含まれています。ELF ヘッダーのStart of program headersとStart of section headersから、セグメントテーブルとセクションテーブルのファイル内の位置を特定できます。
セクションヘッダーテーブル(Section Header Table)#
readelf -S コマンドを使用してバイナリ ELF ファイルのセクション情報(sections)を読み取ります。プログラム test には 31 のセクションがあります。アセンブリ言語はセクションに従ってプログラムを記述します。例えば.text セクション、.data セクション。アセンブリコードは機械コードと一対一で対応しており、アセンブリプログラムがバイナリコードに変換される際にセクション情報が保持されます。
readelf -S test
プログラムヘッダーテーブル(Program Header Table)#
ELF プログラムが実行されるとき(メモリにロードされるとき)、ローダー(Loader)はプログラムのセグメントテーブルに基づいてプロセスのメモリイメージ(Image)を作成します。readelf -l コマンドを使用してバイナリ ELF ファイルのセグメント情報(segments)を読み取ります。プログラム test には 13 のセグメントがあり、セグメントの数はセクションの数よりも多いため、複数のセクションが同じセグメントにマッピングされることがあります。
セクションの権限に基づいて:読み書き可能なセクションは 1 つのセグメントにマッピングされ、読み取り専用のセクションは別のセグメントにマッピングされます。
readelf -l test
リンクビュー / 実行ビュー#
Segment と Section は同じ ELF ファイルを異なる角度から区分けしたものです。これが ELF で異なる ** ビュー(View)** と呼ばれる理由です。
Section の観点から見ると、ELF ファイルは ** リンクビュー(Linking View)です。
Segment の観点から見ると、ELF ファイルは実行ビュー(Execution View)** です。
ELF のロードについて話すとき、セグメントは特に Segment を指します。一方、他の状況ではセグメントは Section を指します。
libc#
glibc:GNU C ライブラリ、glibc 自体は GNU の C 標準ライブラリであり、後に Linux の標準 C ライブラリとなりました。
その拡張子は libc.so であり、本質的には ELF ファイルであり、単独で実行可能です。通常、pwn 問題で接触する動的リンクライブラリは libc.so ファイルです。
Linux のほぼすべてのプログラムは libc に依存しているため、libc 内の関数は非常に重要です。
遅延バインディングメカニズム#
静的コンパイルと動的コンパイル#
動的コンパイルされた実行可能ファイルは、動的リンクライブラリを伴う必要があり、実行時に対応する動的リンクライブラリ内のコマンドを呼び出す必要があります。そのため、利点の一つは実行ファイル自体のサイズを縮小できること、もう一つはコンパイル速度を向上させ、システムリソースを節約できることです。欠点は、非常に単純なプログラムであっても、リンクライブラリ内の 1 つまたは 2 つのコマンドを使用する場合でも、相対的に大きなリンクライブラリを伴う必要があることです。また、他のコンピュータに対応する実行ライブラリがインストールされていない場合、動的コンパイルされた実行可能ファイルは実行できません。
静的コンパイルは、コンパイラが実行可能ファイルをコンパイルする際に、実行可能ファイルが呼び出す必要のある対応する動的リンクライブラリ (.so) の一部を抽出し、実行可能ファイルにリンクすることを意味します。これにより、実行可能ファイルは動的リンクライブラリに依存せずに実行されます。そのため、静的コンパイルの利点と欠点は動的コンパイルの実行可能ファイルと正確に補完し合います。
遅延バインディング(Lazy Binding)#
遅延バインディングを使用するのは、動的リンクの下でプログラムが読み込むモジュールに大量の関数呼び出しが含まれているという前提に基づいています。
遅延バインディングは、関数のアドレスのバインディングをその関数が初めて呼び出されるときまで遅らせることによって、動的リンカーが大量の関数参照の再配置を処理するのを避けます。
遅延バインディングの実装には、2 つの特別なデータ構造が使用されます:グローバルオフセットテーブル(Global Offset Table、GOT)とプロシージャリンクテーブル(Procedure Linkage Table、PLT)。
グローバルオフセットテーブル GOT#
ライブラリ関数が初めて呼び出された後、プログラムはそのアドレスを got テーブルに保存します。
グローバルオフセットテーブルは ELF ファイル内に独立したセクションとして存在し、2 つのクラスを含みます。対応するセクション名は.got
と.got.plt
であり、.got
にはすべての外部変数参照のアドレスが保存され、.got.plt
にはすべての外部関数参照のアドレスが保存されます。遅延バインディングには主に.got.plt
テーブルが使用されます。.got.plt
テーブルの基本構造は以下の図のようになります:
ここで、.got.plt
の最初の 3 項目は特別なアドレス参照を保存します:
- GOT[0]:
.dynamic
セクションのアドレスを保存し、動的リンカーはこのアドレスを利用して動的リンクに関連する情報を抽出します。 - GOT[1]:本モジュールの ID を保存します。
- GOT[2]:動的リンカーの
_dl_runtime_resolve
関数へのアドレスを保存します。この関数は共有ライブラリ関数の実際のシンボルアドレスを解決するために使用されます。
プロシージャリンクテーブル PLT#
遅延バインディングを実現するために、外部モジュールの関数を呼び出す際、プログラムは GOT を直接ジャンプするのではなく、PLT テーブルに保存された特定のテーブル項目を介してジャンプします。すべての外部関数には、PLT テーブルに対応する項目があり、各テーブル項目には特定の関数を呼び出すための 16 バイトのコードが保存されています。プロシージャリンクテーブルの一般的な構造は以下の通りです:
プロシージャリンクテーブルには、コンパイラが呼び出す外部関数のために個別に作成した PLT テーブル項目が含まれているだけでなく、PLT [0] に対応する特別なテーブル項目も含まれています。これは動的リンカーにジャンプし、実際のシンボル解決と再配置作業を行うために使用されます:
PLT と GOT#
外部関数を呼び出す際、プログラムが実際に呼び出すのは PLT テーブルであり、PLT テーブルは実際には一連のアセンブリ命令で構成されています。
では、なぜ PLT が存在し、過渡的である必要があるのか、GOT に直接到達するのではなく?
これは、あなたが多くの親戚を持っている人であると仮定してください。あなたは毎週これらの親戚を訪問する必要があるため、これらの親戚の住所をノートに記録します。訪問する際にノートを見て住所を確認します。このノートが PLT テーブルであり、各アドレスは対応する GOT テーブルのアドレス(あなたの親戚の家)にジャンプします。
ある日、あなたは毎日行き来するのが面倒だと感じ、すべての親戚を自宅に呼び寄せ、毎週対応する部屋に行くだけで済むようにします。この場合、ノートは不要になり、捨ててしまいます。これが PLT テーブルが存在する理由の一つであり、メモリをより効率的に利用するためです。
もう一つの理由は、セキュリティを向上させることです。
LINUX セキュリティ保護メカニズム#
gcc セキュアコンパイルオプションの詳細(NX (DEP)、RELRO、PIE (ASLR)、CANARY、FORTIFY)_gcc pie-CSDN ブログ
CANARY#
Canary はスタックオーバーフロー攻撃に対する保護手段であり、その基本原理はメモリのfs: 0x28から先頭バイトが \x00 で長さ 8 バイトのランダム数canaryをコピーすることです。このランダム数はスタックフレームを作成する際にrbpの直後にスタックに入ります(ebp の上の位置に隣接)。攻撃者がバッファオーバーフローを通じて ebp または ebp の下の戻りアドレスを上書きしようとすると、必ず canary の値も上書きされます。プログラムが終了すると、プログラムは CANARY の値と以前の値が一致するかどうかをチェックし、一致しない場合は実行を続行せず、バッファオーバーフロー攻撃を回避します。
回避方法:
- canary を変更する。
- canary を漏洩させる。
canary バイパス#
- フォーマット文字列による canary のバイパス
- フォーマット文字列を通じて canary の値を読み取る
- Canary のブルートフォース(fork 関数を持つプログラムに対して)
- fork の作用は自己複製に相当し、毎回コピーされたプログラムはメモリレイアウトが同じであり、もちろん canary の値も同じです。そのため、ビットごとにブルートフォース攻撃が可能です。プログラムがクラッシュすれば、そのビットが間違っていることを示し、プログラムが正常に動作すれば次のビットに進むことができます。正しい canary が得られるまで続けます。
- スタックの破損(意図的に canary_ssp 漏洩を引き起こす)
- __stack_chk_fail のハイジャック
- got テーブル内の__stack_chk_fail 関数のアドレスを変更し、スタックオーバーフロー後にその関数を実行しますが、その関数のアドレスが変更されているため、プログラムは実行したいアドレスにジャンプします。
NX#
スタック上のデータは実行権限がありません(not executable)。これを有効にすると、プログラム内のヒープ、スタック、bss セクションなどの書き込み可能なセクションは実行できなくなります。
回避方法:
mprotect 関数を使用してセクションの権限を変更し、nx 保護は rop や got テーブルのハイジャック利用方法には影響しません。
PIE と ASLR#
ASLR とは?#
ASLR は Linux オペレーティングシステムの機能オプションであり、プログラム(ELF)がメモリにロードされて実行されるときに作用します。これはバッファオーバーフローに対するセキュリティ保護技術であり、ロードアドレスのランダム化を通じて、攻撃者が攻撃コードの位置を直接特定するのを防ぎ、オーバーフロー攻撃を阻止する技術です。
ASLR の有効化と無効化#
現在のシステムの ASLR の状態を確認するには:
sudo cat /proc/sys/kernel/randomize_va_space
ASLR には 3 つのセキュリティレベルがあります:
- 0: ASLR が無効
- 1:スタックベースアドレス(stack)、共有ライブラリ(.so ライブラリ)、mmap ベースアドレスをランダム化
- 2:1に基づき、ヒープベースアドレス(chunk)をランダム化
PIE とは?#
PIE は gcc コンパイラの機能オプションであり、プログラム(ELF)のコンパイルプロセスに作用します。これはコードセクション( .text )、データセクション( .data )、未初期化グローバル変数セクション( .bss )などの固定アドレスに対する保護技術であり、プログラムが PIE 保護を有効にすると、プログラムがロードされるたびにロードアドレスが変わり、ROPgadget などのツールを使用して問題を解決することができなくなります。
PIE の有効化#
gcc コンパイル時に-fPIE
オプションを追加します。
PIE が有効になると、コードセクション( .text )、初期化データセクション( .data )、未初期化データセクション( .bss )のロードアドレスがランダム化されます。
PIE バイパス#
プログラムのロードアドレスは通常、メモリページ単位であり、プログラムの基準アドレスの最後の 3 桁は必ず 0 です。これは、既知のアドレスの最後の 3 桁が実際のアドレスの最後の 3 桁であることを意味します。これを知ることで、PIE を回避するための考え方が得られます。完全なアドレスは知らなくても、最後の 3 桁を知っているので、スタック上の既存のアドレスを利用し、最後の 2 桁(最後の 4 桁)だけを変更することができます。
したがって、PIE を回避するための核心的な考え方は ** 部分書き込み(partial writing)** です。
RELRO#
ReLocation Read-Only、スタックアドレスのランダム化は、バイナリデータセクションの保護を強化するための技術です。