システムのセキュリティ対策をテストするために、C関数でスタックアンダーフローを引き起こしたいと思います。インラインアセンブラを使用してこれを行うことができます。しかし、Cはより移植性が高いでしょう。ただし、スタックメモリはその点で言語によって安全に処理されるため、Cを使用してスタックアンダーフローを引き起こす方法は考えられません。
だから、Cを使用して(インラインアセンブラを使用せずに)スタックアンダーフローを引き起こす方法はありますか?
コメントで述べたように、スタックアンダーフローとは、スタックの先頭より下のアドレスを指すスタックポインタを持つことを意味します(スタックが低から高に成長するアーキテクチャでは「下」)。
Cでスタックアンダーフローを引き起こすことが難しいのには、正当な理由があります。その理由は、標準に準拠したCにはスタックがないことです。
C11標準を読んでください。スコープについては説明していますが、スタックについては説明していません。この理由は、規格が実装に関する設計上の決定を強制することを避けるために、可能な限り試みていることです。特定の実装に対して純粋なCでスタックアンダーフローを引き起こす方法を見つけることができるかもしれませんが、未定義の動作または実装固有の拡張機能に依存し、移植性がなくなります。
Cはスタック処理を実装(コンパイラ)に任せるため、これをCで行うことはできません。同様に、スタックに何かをプッシュするが、それをポップするのを忘れる、またはその逆の場合、Cでバグを書くことはできません。
したがって、純粋なCで「スタックアンダーフロー」を生成することは不可能です。Cのスタックからポップすることも、Cからスタックポインタを設定することもできません。スタックの概念は、Cよりもさらに低いレベルにあります。言語。スタックポインターに直接アクセスして制御するには、アセンブラーを記述する必要があります。
Cでcanできることは、意図的にスタックの境界外に書き込むことです。スタックが0x1000から始まり、上に向かって成長することがわかっているとします。次に、これを行うことができます。
volatile uint8_t* const STACK_BEGIN = (volatile uint8_t*)0x1000;
for(volatile uint8_t* p = STACK_BEGIN; p<STACK_BEGIN+n; p++)
{
*p = garbage; // write outside the stack area, at whatever memory comes next
}
アセンブラを使用しない純粋なCプログラムでこれをテストする必要があるのはなぜでしょうか。
上記のコードが未定義の動作を呼び出すという考えを誰かが誤って取得した場合、これはC規格が実際に言っていることです、規範テキストC11 6.5.3.2/4(強調強調):
単項*演算子は、間接指定を示します。オペランドが関数を指している場合、結果は関数指定子になります。オブジェクトを指す場合、結果はオブジェクトを指定する左辺値になります。オペランドのタイプが「 'type to to type'」の場合、結果のタイプは「 'type'」です。 無効な値がポインターに割り当てられている場合、単項*演算子の動作は未定義102)
問題は、「無効な値」の定義とは何か、というのはこれが標準で定義された正式な用語ではないからです。脚注102(参考ではなく参考)にいくつかの例を示します。
単項*演算子によるポインターの逆参照の無効な値には、nullポインター、ポイントされたオブジェクトのタイプに対して不適切に位置合わせされたアドレス、および寿命の終了後のオブジェクトのアドレスがあります。
上記の例では、明らかに、nullポインターも、その寿命の終わりを過ぎたオブジェクトも扱っていません。実際、コードはアクセスの不整合を引き起こす可能性があります-これが問題かどうかは、C標準ではなく実装によって決まります。
そして、「無効な値」の最後のケースは、特定のシステムでサポートされていないアドレスです。特定のシステムのメモリレイアウトはC標準によってカバーされていないため、これは明らかにC標準が言及していることではありません。
Cでスタックアンダーフローを引き起こすことはできません。アンダーフローを引き起こすためには、生成されたコードにプッシュ命令よりもポップ命令が必要です。これは、コンパイラ/インタプリタが正しくないことを意味します。
1980年代には、コンパイルではなく解釈によってCを実行するCの実装がありました。本当にそれらのいくつかは、アーキテクチャによって提供されるスタックの代わりに動的ベクトルを使用しました。
スタックメモリは言語によって安全に処理されます
スタックメモリは、言語ではなく実装によって処理されます。 Cコードを実行し、スタックをまったく使用しないことも可能です。
ISO 9899も K&R も、言語内のスタックの存在について何も指定していません。
トリックを作成してスタックを破壊することは可能ですが、どの実装でも機能せず、一部の実装でのみ機能します。戻りアドレスはスタック上に保持され、それを変更する書き込み許可がありますが、これはアンダーフローでも移植性でもありません。
既に存在する答えについて:搾取緩和技術の文脈で未定義の動作について話すことは適切ではないと思います。
明らかに、実装がスタックアンダーフローに対する緩和策を提供する場合、スタックが提供されます。実際には、void foo(void) { char crap[100]; ... }
はスタック上に配列を持つことになります。
この答えへのコメントによって促されたメモ:未定義の動作は原則であり、原則としてそれを行使するコードは最終的に何かを含む絶対に何でもコンパイルされる可能性があります少しでも元のコードに似ていない。ただし、エクスプロイト緩和手法の対象は、ターゲット環境と実際の動作に密接に関連しています。実際には、以下のコードはうまく機能するはずです。この種のものを扱うときは、必ず生成されたアセンブリを確認する必要があります。
これにより、実際にはアンダーフローが発生します(コンパイラが最適化しないように揮発性が追加されます)。
static void
underflow(void)
{
volatile char crap[8];
int i;
for (i = 0; i != -256; i--)
crap[i] = 'A';
}
int
main(void)
{
underflow();
}
Valgrind 問題をうまく報告します。
定義上、スタックアンダーフローは未定義の動作の一種であるため、このような条件をトリガーするコードはすべてUBでなければなりません。したがって、スタックアンダーフローを確実に引き起こすことはできません。
ただし、次の可変長配列(VLA)の不正使用により、多くの環境で制御可能なスタックアンダーフローが発生します(x86、x86-64、ARMおよびAArch64とClangおよびGCCでテスト済み)、実際にスタックポインタを初期値より上に設定する:
#include <stdint.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
uintptr_t size = -((argc+1) * 0x10000);
char oops[size];
strcpy(oops, argv[0]);
printf("oops: %s\n", oops);
}
これにより、「負の」(非常に大きい)サイズのVLAが割り当てられます。これにより、スタックポインターがラップされ、スタックポインターが上に移動します。 argc
とargv
は、最適化による配列の取り出しを防ぐために使用されます。スタックがダウンすると仮定すると(リストされたアーキテクチャのデフォルト)、これはスタックアンダーフローになります。
strcpy
は、呼び出しが行われたときにアンダーフローアドレスへの書き込みをトリガーするか、strcpy
がインライン化されている場合に文字列が書き込まれます。最後のprintf
には到達できません。
もちろん、これはすべて、VLAを一時的なヒープ割り当てのようなものにしないコンパイラーを想定しています-コンパイラーは完全に自由に実行できます。生成されたアセンブリをチェックして、上記のコードが実際に期待どおりに動作することを確認する必要があります。たとえば、on ARM(gcc -O
):
8428: e92d4800 Push {fp, lr}
842c: e28db004 add fp, sp, #4, 0
8430: e1e00000 mvn r0, r0 ; -argc
8434: e1a0300d mov r3, sp
8438: e0433800 sub r3, r3, r0, lsl #16 ; r3 = sp - (-argc) * 0x10000
843c: e1a0d003 mov sp, r3 ; sp = r3
8440: e1a0000d mov r0, sp
8444: e5911004 ldr r1, [r1]
8448: ebffffc6 bl 8368 <strcpy@plt> ; strcpy(sp, argv[0])
この仮定:
Cはより移植性が高い
真実ではない。 Cは、スタックとその実装での使用方法については何も伝えません。あなたの典型的なx86
プラットフォームでは、次の(恐ろしく無効な)コードが有効なスタックフレームの外側のスタックにアクセスします(OSによって停止されるまで)が、実際には「ポップ」しません。
#include <stdarg.h>
#include <stdio.h>
int underflow(int dummy, ...)
{
va_list ap;
va_start(ap, dummy);
int sum = 0;
for(;;)
{
int x = va_arg(ap, int);
fprintf(stderr, "%d\n", x);
sum += x;
}
return sum;
}
int main(void)
{
return underflow(42);
}
したがって、「スタックアンダーフロー」の正確な意味に応じて、このコードはsomeプラットフォームで必要なことを行います。しかし、Cの観点からすると、これは未定義の動作を公開するだけなので、使用することはお勧めしません。 not "portable"です。
標準準拠のCで確実に行うことは可能ですか?いや
インラインアセンブラに頼らずに、少なくとも1つの実用的なCコンパイラで実行できますか?はい
void * foo(char * a) {
return __builtin_return_address(0);
}
void * bar(void) {
char a[100000];
return foo(a);
}
typedef void (*baz)(void);
int main() {
void * a = bar();
((baz)a)();
}
「-O2 -fomit-frame-pointer -fno-inline」を使用してgccでビルドします
基本的に、このプログラムのフローは次のとおりです。
-fno-inlineが必要です。これは、オプティマイザーのインライン化と慎重に配置された構造の破壊を停止します。また、コンパイラーは、フレームポインターを使用するのではなく、計算によってスタック上のスペースを解放する必要があります。最近のほとんどのgccビルドでは、-fomit-frame-pointerがデフォルトですが、明示的に指定しても問題はありません。
私はこの技術がほぼすべてのCPUアーキテクチャでgccで動作するはずだと信じています。
スタックをアンダーフローさせる方法はありますが、非常に複雑です。私が考えることができる唯一の方法は、一番下の要素へのポインタを定義し、そのアドレス値を減らすことです。つまり*(ptr)-。私の括弧はオフになっているかもしれませんが、ポインターの値をデクリメントしてからポインターを逆参照したい場合があります。
通常、OSはエラーを検出してクラッシュします。何をテストしているかわかりません。これがお役に立てば幸いです。 Cは悪いことをすることを可能にしますが、プログラマーの面倒をみようとします。この保護を回避するほとんどの方法は、ポインターを操作することです。