web-dev-qa-db-ja.com

Cでスタックアンダーフローを引き起こす

システムのセキュリティ対策をテストするために、C関数でスタックアンダーフローを引き起こしたいと思います。インラインアセンブラを使用してこれを行うことができます。しかし、Cはより移植性が高いでしょう。ただし、スタックメモリはその点で言語によって安全に処理されるため、Cを使用してスタックアンダーフローを引き起こす方法は考えられません。

だから、Cを使用して(インラインアセンブラを使用せずに)スタックアンダーフローを引き起こす方法はありますか?

コメントで述べたように、スタックアンダーフローとは、スタックの先頭より下のアドレスを指すスタックポインタを持つことを意味します(スタックが低から高に成長するアーキテクチャでは「下」)。

26
Silicomancer

Cでスタックアンダーフローを引き起こすことが難しいのには、正当な理由があります。その理由は、標準に準拠したCにはスタックがないことです。

C11標準を読んでください。スコープについては説明していますが、スタックについては説明していません。この理由は、規格が実装に関する設計上の決定を強制することを避けるために、可能な限り試みていることです。特定の実装に対して純粋なCでスタックアンダーフローを引き起こす方法を見つけることができるかもしれませんが、未定義の動作または実装固有の拡張機能に依存し、移植性がなくなります。

46
JeremyP

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標準が言及していることではありません。

16
Lundin

Cでスタックアンダーフローを引き起こすことはできません。アンダーフローを引き起こすためには、生成されたコードにプッシュ命令よりもポップ命令が必要です。これは、コンパイラ/インタプリタが正しくないことを意味します。

1980年代には、コンパイルではなく解釈によってCを実行するCの実装がありました。本当にそれらのいくつかは、アーキテクチャによって提供されるスタックの代わりに動的ベクトルを使用しました。

スタックメモリは言語によって安全に処理されます

スタックメモリは、言語ではなく実装によって処理されます。 Cコードを実行し、スタックをまったく使用しないことも可能です。

ISO 9899も K&R も、言語内のスタックの存在について何も指定していません。

トリックを作成してスタックを破壊することは可能ですが、どの実装でも機能せず、一部の実装でのみ機能します。戻りアドレスはスタック上に保持され、それを変更する書き込み許可がありますが、これはアンダーフローでも移植性でもありません。

9
alinsoar

既に存在する答えについて:搾取緩和技術の文脈で未定義の動作について話すことは適切ではないと思います。

明らかに、実装がスタックアンダーフローに対する緩和策を提供する場合、スタックが提供されます。実際には、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が割り当てられます。これにより、スタックポインターがラップされ、スタックポインターが上に移動します。 argcargvは、最適化による配列の取り出しを防ぐために使用されます。スタックがダウンすると仮定すると(リストされたアーキテクチャのデフォルト)、これはスタックアンダーフローになります。

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])
6
nneonneo

この仮定:

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"です。

5
user2371524

標準準拠の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でビルドします

https://godbolt.org/g/GSErDA

基本的に、このプログラムのフローは次のとおりです。

  • メインコールバー。
  • barはスタックにたくさんのスペースを割り当てます(大きな配列のおかげです)。
  • barはfooを呼び出します。
  • fooは、リターンアドレスのコピーを取得します(gcc拡張機能を使用)。このアドレスは、「割り当て」と「クリーンアップ」の間のバーの中央を指します。
  • fooはアドレスをbarに返します。
  • barはスタック割り当てをクリーンアップします。
  • barは、fooによってキャプチャされたリターンアドレスをmainに返します。
  • mainは戻りアドレスを呼び出して、バーの中央にジャンプします。
  • barからのスタッククリーンアップコードは実行されますが、barには現在スタックフレームがありません(その途中にジャンプしたため)。そのため、スタッククリーンアップコードはスタックをアンダーフローします。

-fno-inlineが必要です。これは、オプティマイザーのインライン化と慎重に配置された構造の破壊を停止します。また、コンパイラーは、フレームポインターを使用するのではなく、計算によってスタック上のスペースを解放する必要があります。最近のほとんどのgccビルドでは、-fomit-frame-pointerがデフォルトですが、明示的に指定しても問題はありません。

私はこの技術がほぼすべてのCPUアーキテクチャでgccで動作するはずだと信じています。

4
plugwash

スタックをアンダーフローさせる方法はありますが、非常に複雑です。私が考えることができる唯一の方法は、一番下の要素へのポインタを定義し、そのアドレス値を減らすことです。つまり*(ptr)-。私の括弧はオフになっているかもしれませんが、ポインターの値をデクリメントしてからポインターを逆参照したい場合があります。

通常、OSはエラーを検出してクラッシュします。何をテストしているかわかりません。これがお役に立てば幸いです。 Cは悪いことをすることを可能にしますが、プログラマーの面倒をみようとします。この保護を回避するほとんどの方法は、ポインターを操作することです。

0