web-dev-qa-db-ja.com

セキュリティのためにヒープがゼロで初期化されている場合、スタックは単に初期化されていないのですか?

私のDebian GNU/Linux 9システムで、バイナリが実行されると、

  • スタックは初期化されていませんが
  • ヒープはゼロで初期化されます。

どうして?

ゼロ初期化はセキュリティを促進すると思いますが、ヒープの場合は、なぜスタックもそうでないのですか?スタックもセキュリティを必要としませんか?

私の知る限り、私の質問はDebianに固有のものではありません。

サンプルCコード:

_#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 8;

// --------------------------------------------------------------------
// UNINTERESTING CODE
// --------------------------------------------------------------------
static void print_array(
  const int *const p, const size_t size, const char *const name
)
{
    printf("%s at %p: ", name, p);
    for (size_t i = 0; i < size; ++i) printf("%d ", p[i]);
    printf("\n");
}

// --------------------------------------------------------------------
// INTERESTING CODE
// --------------------------------------------------------------------
int main()
{
    int a[n];
    int *const b = malloc(n*sizeof(int));
    print_array(a, n, "a");
    print_array(b, n, "b");
    free(b);
    return 0;
}
_

出力:

_a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713 
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0 
_

もちろん、C標準では、メモリを割り当てる前にmalloc()にメモリをクリアするように求めていませんが、私のCプログラムは単に説明用です。この質問は、CやCの標準ライブラリに関する質問ではありません。むしろ、問題は、カーネルやランタイムローダーがヒープではなくスタックをゼロにしている理由についての問題です。

別の実験

私の質問は、標準ドキュメントの要件ではなく、観察可能なGNU/Linuxの動作に関するものです。私の意味がわからない場合は、このコードを試してください。これにより、未定義の動作(undefined、、つまりC標準に関する限り)が呼び出され、ポイントを説明します:

_#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(sizeof(int));
        printf("%p %d ", p, *p);
        ++*p;
        printf("%d\n", *p);
        free(p);
    }
    return 0;
}
_

私のマシンからの出力:

_0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
_

C標準に関する限り、動作は定義されていないため、私の質問ではC標準は考慮していません。 malloc()への呼び出しは毎回同じアドレスを返す必要はありませんが、このmalloc()への呼び出しは確かに毎回同じアドレスを返すため、ヒープ上にあるメモリは毎回ゼロにされます。

対照的に、スタックはゼロにされたようには見えませんでした。

GNU/Linuxシステムのどの層が観察された動作を引き起こしているのかわからないので、後者のコードがあなたのマシンで何をするかわかりません。あなたはそれを試すことができます。

[〜#〜]更新[〜#〜]

@Kusalanandaはコメントで観察しています:

価値があるのは、OpenBSDで実行すると、最新のコードが異なるアドレスと(時折)初期化されていない(ゼロ以外の)データを返すことです。これは明らかに、Linuxで目撃している動作については何も述べていません。

私の結果がOpenBSDの結果と異なることは確かに興味深いです。どうやら、私の実験では、思ったようにカーネル(またはリンカー)セキュリティプロトコルではなく、単なる実装上のアーティファクトが発見されました。

この観点から、@ mosvy、@ StephenKitt、および@AndreasGrapentinの以下の回答が一緒になって私の質問を解決すると思います。

スタックオーバーフローもご覧ください: gallocでmallocが値を0に初期化する理由 (クレジット:@bta)。

16
thb

Malloc()によって返されるストレージはnotゼロで初期化されます。それがそうであると決して考えないでください。

あなたのテストプログラムでは、それはまぐれです:malloc()mmap()から新しいブロックを取得したと思いますが、それに依存しないでください。

例として、私のマシンでこのようにプログラムを実行すると、次のようになります。

$ echo 'void __attribute__((constructor)) p(void){
    void *b = malloc(4444); memset(b, 4, 4444); free(b);
}' | cc -include stdlib.h -include string.h -xc - -shared -o pollute.so

$ LD_PRELOAD=./pollute.so ./your_program
a at 0x7ffd40d3aa60: 1256994848 21891 1256994464 21891 1087613792 32765 0 0
b at 0x55834c75d010: 67372036 67372036 67372036 67372036 67372036 67372036 67372036 67372036

2番目の例は、glibcのmalloc実装のアーティファクトを公開するだけです。 8バイトより大きいバッファーでmalloc/freeを繰り返し実行すると、次のサンプルコードのように、最初の8バイトのみがゼロ化されていることがはっきりとわかります。

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;
const size_t m = 0x10;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(m*sizeof(int));
        printf("%p ", p);
        for (size_t j = 0; j < m; ++j) {
            printf("%d:", p[j]);
            ++p[j];
            printf("%d ", p[j]);
        }
        free(p);
        printf("\n");
    }
    return 0;
}

出力:

0x55be12864010 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 
0x55be12864010 0:1 0:1 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 
0x55be12864010 0:1 0:1 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 
0x55be12864010 0:1 0:1 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4
30
mosvy

スタックの初期化方法に関係なく、Cライブラリはmainを呼び出す前にいくつかの処理を行い、スタックにアクセスするため、元のスタックは表示されません。

GNU Cライブラリ、x86-64では、- _ start エントリポイントから実行が開始され、- __libc_start_main セットアップを行い、後者はmainを呼び出します。しかし、mainを呼び出す前に、他の多くの関数を呼び出します。これにより、さまざまなデータがスタックに書き込まれます。スタックの内容は関数呼び出し間でクリアされないため、mainに入ると、スタックには前の関数呼び出しの残りが含まれています。

これは、スタックから得られる結果を説明するだけです。一般的なアプローチと仮定に関する他の回答を参照してください。

25
Stephen Kitt

どちらの場合も、ninitializedメモリーが表示され、その内容について何も想定できません。

OSが新しいページをプロセスに割り当てる必要がある場合(それがそのスタック用か、malloc()が使用するアリーナ用か)、それは他のプロセスからのデータを公開しないことを保証します。それを確実にする通常の方法は、それをゼロで埋めることです(ただし、/dev/urandomに相当するページも含めて、他のもので上書きすることも同様に有効です-実際、一部のデバッグmalloc()実装はゼロ以外のパターンを書き込みます、あなたのような誤った仮定をキャッチするために)。

malloc()がこのプロセスによってすでに使用および解放されているメモリからの要求を満たすことができる場合、その内容はクリアされません(実際、クリアはmalloc()とは関係なく、それはできません-メモリがアドレス空間にマッピングされる前に発生する必要があります)。プロセス/プログラムによって以前に書き込まれたメモリを取得する可能性があります(例:main()の前)。

サンプルプログラムでは、このプロセスによってまだ書き込まれていない(つまり、新しいページから直接)malloc()領域と、(pre -main()プログラム内のコード)。スタックをさらに調べると、さらに下に(成長方向に)ゼロで埋められていることがわかります。

OSレベルで何が起こっているのかを本当に理解したい場合は、Cライブラリレイヤーをバイパスし、代わりにbrk()mmap()などのシステムコールを使用して対話することをお勧めします。

19
Toby Speight

あなたの前提は間違っています。

「セキュリティ」とは、実際にはconfidentialityです。つまり、これらのプロセス間でメモリが明示的に共有されていない限り、プロセスは別のプロセスのメモリを読み取ることができません。オペレーティングシステムでは、これは並行アクティビティまたはプロセスの分離の1つの側面です。

この分離を確実にするためにオペレーティングシステムが実行していることは、ヒープまたはスタックの割り当てのためにプロセスによってメモリが要求されるたびに、このメモリは、ゼロで埋められた物理メモリ内の領域から、または同じプロセスから来る。

これにより、ゼロまたは独自のジャンクのみが表示されることが保証されるため、機密性が保証され、bothヒープandスタックは「安全」ですが、必ずしも(ゼロ-)初期化されました。

測定値を読みすぎています。

9