it-swarm.com.de

Wenn der Heap aus Sicherheitsgründen mit Null initialisiert ist, warum ist der Stack dann nur nicht initialisiert?

Wenn auf meinem Debian GNU/Linux 9-System eine Binärdatei ausgeführt wird,

  • der Stack ist aber nicht initialisiert
  • der Heap ist mit Null initialisiert.

Warum?

Ich gehe davon aus, dass die Nullinitialisierung die Sicherheit fördert, aber wenn nicht für den Heap, warum dann nicht auch für den Stack? Braucht auch der Stack keine Sicherheit?

Meine Frage ist meines Wissens nicht spezifisch für Debian.

Beispiel C-Code:

#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;
}

Ausgabe:

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

Der C-Standard fordert malloc() natürlich nicht auf, den Speicher zu löschen, bevor er zugewiesen wird, aber mein C-Programm dient nur zur Veranschaulichung. Die Frage ist keine Frage zu C oder zur Standardbibliothek von C. Die Frage ist vielmehr eine Frage, warum der Kernel und/oder der Laufzeitlader den Heap auf Null setzen, nicht aber den Stapel.

EIN ANDERES EXPERIMENT

Meine Frage bezieht sich eher auf das beobachtbare GNU/Linux-Verhalten als auf die Anforderungen von Standarddokumenten. Wenn Sie sich nicht sicher sind, was ich meine, versuchen Sie diesen Code, der weiteres undefiniertes Verhalten hervorruft ( undefiniert , das heißt, was den C-Standard betrifft), um den Punkt zu veranschaulichen:

#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;
}

Ausgabe von meiner Maschine:

0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1

In Bezug auf den C-Standard ist das Verhalten undefiniert, daher bezieht sich meine Frage nicht auf den C-Standard. Ein Aufruf von malloc() muss nicht jedes Mal dieselbe Adresse zurückgeben, aber da dieser Aufruf von malloc() tatsächlich jedes Mal dieselbe Adresse zurückgibt, ist es interessant festzustellen, dass Der Speicher, der sich auf dem Heap befindet, wird jedes Mal auf Null gesetzt.

Im Gegensatz dazu schien der Stapel nicht auf Null gesetzt zu sein.

Ich weiß nicht, was der letztere Code auf Ihrem Computer tun wird, da ich nicht weiß, welche Schicht des GNU/Linux-Systems das beobachtete Verhalten verursacht. Sie können es aber versuchen.

[~ # ~] Update [~ # ~]

@Kusalananda hat in Kommentaren beobachtet:

Für das, was es wert ist, gibt Ihr aktueller Code verschiedene Adressen und (gelegentlich) nicht initialisierte (nicht null) Daten zurück, wenn er unter OpenBSD ausgeführt wird. Dies sagt offensichtlich nichts über das Verhalten aus, das Sie unter Linux beobachten.

Dass sich mein Ergebnis vom Ergebnis auf OpenBSD unterscheidet, ist in der Tat interessant. Anscheinend entdeckten meine Experimente kein Kernel oder Linker-) Sicherheitsprotokoll, wie ich gedacht hatte, sondern nur ein Implementierungsartefakt.

Vor diesem Hintergrund glaube ich, dass die folgenden Antworten von @mosvy, @StephenKitt und @AndreasGrapentin meine Frage zusammen regeln.

Siehe auch zum Stapelüberlauf: --- (Warum initialisiert malloc die Werte in gcc auf 0? (credit: @bta).

16
thb

Der von malloc () zurückgegebene Speicher ist nicht nullinitialisiert. Gehen Sie niemals davon aus, dass dies der Fall ist.

In Ihrem Testprogramm ist es nur ein Zufall: Ich denke, die malloc() hat gerade einen neuen Block von mmap() bekommen, aber verlassen Sie sich auch nicht darauf.

Zum Beispiel, wenn ich Ihr Programm auf meinem Computer folgendermaßen ausführe:

$ 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

Ihr zweites Beispiel besteht darin, einfach ein Artefakt der Implementierung von malloc in glibc verfügbar zu machen. Wenn Sie dies wiederholt malloc/free mit einem Puffer größer als 8 Byte tun, werden Sie deutlich sehen, dass nur die ersten 8 Bytes auf Null gesetzt werden, wie im folgenden Beispielcode.

#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;
}

Ausgabe:

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

Unabhängig davon, wie der Stapel initialisiert wird, wird kein makelloser Stapel angezeigt, da die C-Bibliothek vor dem Aufruf von main eine Reihe von Aufgaben ausführt und den Stapel berührt.

Mit der Bibliothek GNU C) auf x86-64 beginnt die Ausführung am Einstiegspunkt _ start , der __libc_start_main um Dinge einzurichten, und letztere ruft am Ende main auf. Vor dem Aufruf von main werden jedoch eine Reihe anderer Funktionen aufgerufen, wodurch verschiedene Daten in den Stapel geschrieben werden. Der Inhalt des Stapels wird zwischen den Funktionsaufrufen nicht gelöscht. Wenn Sie also zu main gelangen, enthält Ihr Stapel Reste der vorherigen Funktionsaufrufe.

Dies erklärt nur die Ergebnisse, die Sie vom Stapel erhalten. Weitere Antworten zu Ihrem allgemeinen Ansatz und Ihren Annahmen finden Sie in den anderen Antworten.

25
Stephen Kitt

In beiden Fällen erhalten Sie nicht initialisiert Speicher, und Sie können keine Annahmen über dessen Inhalt treffen.

Wenn das Betriebssystem Ihrem Prozess eine neue Seite zuordnen muss (sei es für seinen Stapel oder für die von malloc() verwendete Arena), wird garantiert, dass keine Daten aus anderen Prozessen verfügbar gemacht werden. Der übliche Weg, dies sicherzustellen, besteht darin, es mit Nullen zu füllen (aber es ist ebenso gültig, es mit irgendetwas anderem zu überschreiben, einschließlich sogar einer Seite im Wert von /dev/urandom - tatsächlich schreiben einige Debugging-Implementierungen malloc() Muster ungleich Null, um sie abzufangen falsche Annahmen wie Ihre).

Wenn malloc() die Anforderung aus dem Speicher erfüllen kann, der bereits von diesem Prozess verwendet und freigegeben wurde, wird sein Inhalt nicht gelöscht (tatsächlich hat das Löschen nichts mit malloc() zu tun und kann es auch nicht sein - es muss geschehen, bevor der Speicher vorhanden ist in Ihren Adressraum abgebildet). Möglicherweise erhalten Sie Speicher, der zuvor von Ihrem Prozess/Programm geschrieben wurde (z. B. vor main()).

In Ihrem Beispielprogramm sehen Sie eine Region malloc(), die von diesem Prozess noch nicht geschrieben wurde (d. H. Direkt von einer neuen Seite stammt), und einen Stapel, in den geschrieben wurde (durch den Code vor -main() in Ihrem Programm). Wenn Sie mehr von dem Stapel untersuchen, werden Sie feststellen, dass er weiter unten (in seiner Wachstumsrichtung) mit Null gefüllt ist.

Wenn Sie wirklich verstehen möchten, was auf Betriebssystemebene geschieht, empfehlen wir Ihnen, die C-Bibliotheksebene zu umgehen und stattdessen mit Systemaufrufen wie brk() und mmap() zu interagieren.

19
Toby Speight

Ihre Prämisse ist falsch.

Was Sie als "Sicherheit" beschreiben, ist wirklich Vertraulichkeit, was bedeutet, dass kein Prozess einen anderen Prozessspeicher lesen darf, es sei denn, dieser Speicher wird explizit von diesen Prozessen gemeinsam genutzt. In einem Betriebssystem ist dies ein Aspekt der Isolation gleichzeitiger Aktivitäten oder Prozesse.

Um diese Isolation sicherzustellen, führt das Betriebssystem immer dann, wenn der Prozess Speicher für Heap- oder Stapelzuweisungen anfordert, dieser Speicher entweder aus einem Bereich im physischen Speicher, der mit Nullen gefüllt ist, oder der mit Junk gefüllt ist kommt von der gleiche Prozess.

Dies stellt sicher, dass Sie immer nur Nullen oder Ihren eigenen Müll sehen, sodass die Vertraulichkeit gewährleistet ist und beide Heap nd Stapel 'sicher' sind, wenn auch nicht unbedingt (Null) -) initialisiert.

Sie lesen zu viel in Ihre Messungen.

9