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


15

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の動作に関するものです。意味がわからない場合は、このコードを試してください。これにより、さらに未定義の動作(未定義、つまり、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の以下の回答が一緒になって私の疑問を解決すると信じています。

Stack Overflowも参照してください:なぜmallocはgccで値を0に初期化するのですか?(クレジット:@bta)。


2
価値のあるものとして、OpenBSDで実行すると、最新のコードは異なるアドレスと(ときどき)初期化されていない(ゼロでない)データを返します。これは明らかに、Linuxで目撃している動作については何も言っていませ
クサラナナンダ

質問の範囲を変更したり、回答やコメントを冗長にするために質問を編集したりしないでください。Cでは、「ヒープ」はmalloc()およびcalloc()によって返されるメモリ以外の何物でもありません。後者だけがメモリをゼロにします。newC ++ の演算子(「ヒープ」)はLinuxではmalloc()の単なるラッパーです。カーネルは「ヒープ」が何であるかを知りません。
mosvy

3
2番目の例は、glibcのmalloc実装のアーティファクトを単に公開することです。8バイトを超えるバッファーでmalloc / freeを繰り返し実行すると、最初の8バイトのみがゼロになっていることが明確にわかります。
mosvy

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

@thbこれは正しい観測であると信じています、はい。
クサラナナンダ

回答:


28

malloc()によって返されるストレージはゼロで初期化されません。決してそうだと思い込まないでください。

あなたのテストプログラムでは、それは単なるまぐれです:私は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番目の例は、単にmallocglibc の実装の成果物を公開することです。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

2
ええ、はい、しかし、これがスタックオーバーフローではなくここで質問をした理由です。私の質問は、C標準に関するものではなく、最新のGNU / Linuxシステムが通常バイナリをリンクおよびロードする方法に関するものでした。あなたのLD_PRELOADはユーモラスですが、私が尋ねるつもりだった質問とは別の質問に答えます。
-thb

19
私はあなたを笑わせてうれしいですが、あなたの仮定と偏見は全く面白くありません。「最新のGNU / Linuxシステム」では、バイナリは通常、プログラムからmain()関数に到達する前に動的ライブラリからコンストラクタを実行している動的リンカーによってロードされます。Debian GNU / Linux 9システムでは、プリロードされたライブラリを使用していない場合でも、プログラムのmain()関数の前にmalloc()とfree()の両方が複数回呼び出されます。
mosvy

23

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

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

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


特にC ++ライブラリがリンクされている場合main()は、呼び出されるまでに、初期化ルーチンがメモリを変更する可能性が非常に高いことに注意してくださいmalloc()
アンドリューヘンレ

モスビーと一緒にあなたの答えは私の質問を解決します。残念ながら、このシステムでは2つのうち1つしか受け入れられません。そうでなければ、私は両方を受け入れます。
thb

18

どちらの場合も、初期化されていないメモリを取得し、その内容について推測することはできません。

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

malloc()このプロセスによって既に使用および解放されているメモリからの要求を満たすことができる場合、その内容はクリアされません(実際、クリアは処理とは無関係であり、実行するmalloc()ことはできません。あなたのアドレス空間)。プロセス/プログラムによって以前に書き込まれたメモリを取得する場合があります(たとえばの前main())。

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

あなたが本当にOSレベルで何が起こっているか理解したい場合は、私はあなたのCライブラリ層バイパスやシステムを使用して相互作用することをお勧めしますような呼び出しをbrk()し、mmap()代わりに。


1
1、2週間前、私は別の実験を試み、電話をかけmalloc()free()繰り返しました。malloc()最近解放された同じストレージを再利用する必要はありませんが、実験でmalloc()はそれを実現しました。毎回同じアドレスを返すことがありましたが、毎回メモリを無効にしました。これは私にとって興味深いものでした。さらなる実験が今日の問題につながっています。
thb

1
@thb、おそらく私は十分に明確ではありません-のほとんどの実装は、彼らがあなたに渡すメモリでmalloc()絶対に何もしません-それは以前に使用されたか、新たに割り当てられました(したがってOSによってゼロにされました)。テストでは、明らかに後者を取得しました。同様に、スタックメモリはクリアされた状態でプロセスに与えられますが、プロセスがまだ触れていない部分を確認するために十分に調べないでください。スタックメモリ、プロセスに渡される前にクリアされます。
トビースパイト

2
@TobySpeight:brkとsbrkはmmapによって廃止されました。pubs.opengroup.org/onlinepubs/7908799/xsh/brk.htmlは、最上位にLEGACYとあります。
ジョシュア

2
あなたが使用して初期化されたメモリを必要とする場合calloc(代わりのオプションであるかもしれないmemset
eckes

2
@thb and Toby:楽しい事実:カーネルからの新しいページは、しばしば遅延的に割り当てられ、共有されたゼロ化されたページにコピーオンライトマッピングされるだけです。これは、同様にmmap(MAP_ANONYMOUS)使用しない限り起こりMAP_POPULATEます。通常、新しいスタックメモリは最初にタッチされたときに書き込まれるため、新しいスタックページは新しい物理ページによってバックアップされ、(ハードウェアページテーブルにマッピングされ、カーネルのポインター/マッピングの長さリストと同様に)ワイヤリングされます。しかし、はい、カーネルはなんらかの方法でデータの漏洩を回避する必要があり、ゼロ化は最も安価で便利です。
ピーター

9

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

あなたが「セキュリティ」と呼んでいるものは本当に機密性です。つまり、これらのプロセス間でメモリが明示的に共有されない限り、どのプロセスも他のプロセスメモリを読み取れないことを意味します。オペレーティングシステムでは、これは同時アクティビティまたはプロセスの分離の 1つの側面です。

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

これにより、ゼロまたは独自のジャンクのみが表示されるため、機密性が確保され、ヒープスタックの両方が「セキュア」になりますが、必ずしも(ゼロ)で初期化されるわけではありません。

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


1
質問の「更新」セクションでは、明確な回答が明示的に参照されます。
-thb
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.