Jika heap diinisialisasi nol untuk keamanan, lalu mengapa stack hanya diinisialisasi?


15

Di sistem Debian GNU / Linux 9 saya, ketika biner dijalankan,

  • stack belum diinisialisasi tetapi
  • heap diinisialisasi nol.

Mengapa?

Saya berasumsi bahwa inisialisasi nol mempromosikan keamanan tetapi, jika untuk heap, lalu mengapa tidak juga untuk stack? Apakah tumpukan juga tidak membutuhkan keamanan?

Pertanyaan saya tidak spesifik untuk Debian sejauh yang saya tahu.

Contoh kode 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;
}

Keluaran:

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

Standar C tidak meminta malloc()untuk menghapus memori sebelum mengalokasikannya, tentu saja, tetapi program C saya hanya untuk ilustrasi. Pertanyaannya bukan pertanyaan tentang C atau tentang perpustakaan standar C. Sebaliknya, pertanyaannya adalah pertanyaan tentang mengapa kernel dan / atau run-time loader memusatkan perhatian pada tumpukan tetapi bukan tumpukan.

EKSPERIMEN LAIN

Pertanyaan saya mengenai perilaku GNU / Linux yang dapat diamati daripada persyaratan dokumen standar. Jika tidak yakin apa yang saya maksud, maka coba kode ini, yang memanggil perilaku tidak terdefinisi lebih lanjut ( tidak terdefinisi, yaitu sejauh menyangkut standar C) untuk menggambarkan poin:

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

Output dari mesin saya:

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

Sejauh menyangkut standar C, perilaku tidak terdefinisi, jadi pertanyaan saya tidak mempertimbangkan standar C. Panggilan untuk malloc()tidak perlu mengembalikan alamat yang sama setiap kali tetapi, karena panggilan untuk malloc()ini memang benar-benar mengembalikan alamat yang sama setiap kali, menarik untuk memperhatikan bahwa memori, yang ada di heap, adalah nol setiap kali.

Tumpukan itu, sebaliknya, tampaknya tidak memusatkan perhatian.

Saya tidak tahu apa yang akan dilakukan kode terakhir pada mesin Anda, karena saya tidak tahu lapisan mana dari sistem GNU / Linux yang menyebabkan perilaku yang diamati. Anda bisa mencobanya.

MEMPERBARUI

@ Kusalananda telah mengamati dalam komentar:

Untuk apa nilainya, kode terbaru Anda mengembalikan alamat yang berbeda dan (sesekali) data yang tidak diinisialisasi (bukan nol) ketika dijalankan di OpenBSD. Ini jelas tidak mengatakan apa-apa tentang perilaku yang Anda saksikan di Linux.

Bahwa hasil saya berbeda dengan hasil di OpenBSD memang menarik. Rupanya, percobaan saya bukan menemukan protokol keamanan kernel (atau linker), seperti yang saya pikirkan, tetapi hanya artefak implementasi.

Dalam terang ini, saya percaya bahwa, bersama-sama, jawaban di bawah @mosvy, @StephenKitt dan @AndreasGrapentin menyelesaikan pertanyaan saya.

Lihat juga di Stack Overflow: Mengapa malloc menginisialisasi nilai ke 0 dalam gcc? (kredit: @bta).


2
Untuk apa nilainya, kode terbaru Anda mengembalikan alamat yang berbeda dan (sesekali) data yang tidak diinisialisasi (bukan nol) ketika dijalankan di OpenBSD. Ini jelas tidak mengatakan apa-apa tentang perilaku yang Anda saksikan di Linux.
Kusalananda

Tolong jangan mengubah ruang lingkup pertanyaan Anda, dan jangan mencoba mengeditnya untuk membuat jawaban dan komentar berlebihan. Dalam C, "heap" tidak lain adalah memori yang dikembalikan oleh malloc () dan calloc (), dan hanya yang terakhir yang memusatkan memori; yang newoperator dalam C ++ (juga "tumpukan") adalah di Linux hanya bungkus untuk malloc (); kernel tidak tahu atau tidak peduli apa "heap" itu.
Mosvy

3
Contoh kedua Anda hanya mengekspos artefak implementasi malloc di glibc; jika Anda melakukan itu berulang malloc / gratis dengan buffer lebih besar dari 8 byte, Anda akan jelas melihat bahwa hanya 8 byte pertama yang di-zeroed.
Mosvy

@ Kusalananda, begitu. Bahwa hasil saya berbeda dengan hasil di OpenBSD memang menarik. Tampaknya, Anda dan Mosvy telah menunjukkan bahwa percobaan saya bukan menemukan protokol keamanan kernel (atau penghubung), seperti yang saya pikirkan, tetapi hanya artefak implementasi.
thb

@ thb Saya percaya bahwa ini mungkin pengamatan yang benar, ya.
Kusalananda

Jawaban:


28

Penyimpanan dikembalikan oleh malloc () tidak diinisialisasi nol. Jangan pernah berasumsi itu.

Dalam program pengujian Anda, itu hanya kebetulan: Saya kira malloc()baru saja mendapat blok baru mmap(), tetapi jangan mengandalkan itu juga.

Sebagai contoh, jika saya menjalankan program Anda di komputer saya dengan cara ini:

$ 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

Contoh kedua Anda hanya mengekspos artefak mallocimplementasi di glibc; jika Anda melakukannya berulang malloc/ freedengan buffer lebih besar dari 8 byte, Anda akan melihat dengan jelas bahwa hanya 8 byte pertama yang di-zeroed, seperti pada kode contoh berikut.

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

Keluaran:

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
Ya, tapi inilah mengapa saya mengajukan pertanyaan di sini daripada di Stack Overflow. Pertanyaan saya bukan tentang standar C tetapi tentang cara sistem GNU / Linux modern biasanya menautkan dan memuat binari. LD_PRELOAD Anda lucu tetapi menjawab pertanyaan lain selain pertanyaan yang ingin saya tanyakan.
thb

19
Saya senang saya membuat Anda tertawa, tetapi asumsi dan prasangka Anda tidak lucu sama sekali. Pada "sistem GNU / Linux modern", binari biasanya dimuat oleh tautan dinamis, yang menjalankan konstruktor dari perpustakaan dinamis sebelum sampai ke fungsi utama () dari program Anda. Pada sistem GNU / Linux 9 Debian Anda, baik malloc () dan free () akan dipanggil lebih dari sekali sebelum fungsi utama () dari program Anda, bahkan ketika tidak menggunakan pustaka yang dimuat sebelumnya.
Mosvy

23

Terlepas dari bagaimana tumpukan diinisialisasi, Anda tidak melihat tumpukan yang asli, karena perpustakaan C melakukan beberapa hal sebelum memanggil main, dan mereka menyentuh tumpukan.

Dengan pustaka GNU C, pada x86-64, eksekusi dimulai pada titik entri _start , yang memanggil __libc_start_mainuntuk mengatur segalanya, dan yang terakhir akhirnya memanggil main. Tetapi sebelum memanggil main, ia memanggil sejumlah fungsi lain, yang menyebabkan berbagai bagian data ditulis ke stack. Isi tumpukan tidak dihapus di antara panggilan fungsi, jadi ketika Anda masuk main, tumpukan Anda berisi sisa dari panggilan fungsi sebelumnya.

Ini hanya menjelaskan hasil yang Anda dapatkan dari tumpukan, lihat jawaban lain mengenai pendekatan dan asumsi umum Anda.


Perhatikan bahwa pada saat main()dipanggil, rutin inisialisasi mungkin telah memodifikasi memori yang dikembalikan oleh malloc()- terutama jika pustaka C ++ terhubung. Dengan asumsi "heap" diinisialisasi ke apa pun adalah asumsi yang benar-benar buruk.
Andrew Henle

Jawaban Anda bersama Mosvy menjawab pertanyaan saya. Sayangnya sistem ini memungkinkan saya untuk menerima hanya satu dari keduanya; kalau tidak, saya akan menerima keduanya.
thb

18

Dalam kedua kasus, Anda mendapatkan memori yang tidak diinisialisasi , dan Anda tidak dapat membuat asumsi tentang isinya.

Ketika OS harus membagi halaman baru ke proses Anda (apakah itu untuk tumpukan atau untuk arena yang digunakan oleh malloc()), itu menjamin bahwa ia tidak akan mengekspos data dari proses lain; cara biasa untuk memastikan itu adalah mengisinya dengan nol (tetapi sama-sama valid untuk menimpa dengan hal lain, termasuk bahkan nilai halaman /dev/urandom- pada kenyataannya beberapa malloc()implementasi debugging menulis pola non-nol, untuk menangkap asumsi yang salah seperti milik Anda).

Jika malloc()dapat memenuhi permintaan dari memori yang sudah digunakan dan dirilis oleh proses ini, isinya tidak akan dihapus (sebenarnya, kliring tidak ada hubungannya dengan malloc()dan tidak bisa - itu harus terjadi sebelum memori dipetakan ke dalam ruang alamat Anda). Anda mungkin mendapatkan memori yang sebelumnya telah ditulis oleh proses / program Anda (misalnya sebelumnya main()).

Dalam program contoh Anda, Anda melihat malloc()wilayah yang belum ditulis oleh proses ini (yaitu langsung dari halaman baru) dan tumpukan yang telah ditulis ke (dengan main()kode pra -program Anda). Jika Anda memeriksa lebih banyak tumpukan, Anda akan menemukan nol-diisi lebih jauh ke bawah (ke arah pertumbuhan).

Jika Anda benar-benar ingin memahami apa yang terjadi di tingkat OS, saya sarankan Anda melewati lapisan C Library dan berinteraksi menggunakan panggilan sistem seperti brk()dan sebagai mmap()gantinya.


1
Satu atau dua minggu yang lalu, saya mencoba percobaan yang berbeda, menelepon malloc()dan free()berulang kali. Meskipun tidak ada yang perlu malloc()menggunakan kembali penyimpanan yang sama yang baru-baru ini dibebaskan, dalam percobaan, malloc()memang melakukan itu. Itu terjadi untuk mengembalikan alamat yang sama setiap kali, tetapi juga membatalkan memori setiap kali, yang saya tidak harapkan. Ini menarik bagi saya. Eksperimen lebih lanjut telah menimbulkan pertanyaan hari ini.
thb

1
@ THB, Mungkin saya tidak cukup jelas - sebagian besar implementasi malloc()benar-benar tidak melakukan apa - apa dengan memori yang mereka berikan kepada Anda - itu baik yang sebelumnya digunakan, atau baru ditugaskan (dan karena itu memusatkan perhatian oleh OS). Dalam tes Anda, Anda jelas mendapatkan yang terakhir. Demikian pula, memori tumpukan diberikan untuk proses Anda dalam keadaan dihapus, tetapi Anda tidak memeriksanya cukup jauh untuk melihat bagian-bagian proses Anda belum menyentuh. Stack memori Anda adalah dibersihkan sebelum itu diberikan kepada proses Anda.
Toby Speight

2
@TobySpeight: brk dan sbrk sudah usang oleh mmap. pubs.opengroup.org/onlinepubs/7908799/xsh/brk.html mengatakan LEGACY tepat di atas.
Joshua

2
Jika Anda membutuhkan memori yang diinisialisasi menggunakan callocmungkin menjadi pilihan (bukan memset)
eckes

2
@ THB dan Toby: fakta menyenangkan: halaman baru dari kernel sering malas dialokasikan, dan hanya copy-on-write dipetakan ke halaman zeroed bersama. Ini terjadi mmap(MAP_ANONYMOUS)kecuali Anda menggunakannya MAP_POPULATEjuga. Mudah-mudahan halaman stack baru didukung oleh halaman fisik baru dan ditransfer (dipetakan dalam tabel halaman perangkat keras, serta pointer / daftar panjang pemetaan kernel) ketika tumbuh, karena biasanya memori stack baru sedang ditulis ketika disentuh untuk pertama kalinya . Tetapi ya, kernel harus menghindari kebocoran data, dan nol adalah yang termurah dan paling berguna.
Peter Cordes

9

Premis Anda salah.

Apa yang Anda gambarkan sebagai 'keamanan' adalah benar-benar kerahasiaan , artinya tidak ada proses yang dapat membaca memori proses lainnya, kecuali jika memori ini dibagi secara eksplisit di antara proses-proses ini. Dalam sistem operasi, ini adalah salah satu aspek dari isolasi kegiatan bersamaan, atau proses.

Apa yang dilakukan sistem operasi untuk memastikan isolasi ini, adalah setiap kali memori diminta oleh proses untuk alokasi tumpukan atau tumpukan, memori ini berasal dari suatu wilayah dalam memori fisik yang diisi dengan nol, atau yang diisi dengan sampah yang berasal dari proses yang sama .

Hal ini memastikan bahwa Anda hanya pernah melihat nol, atau junk Anda sendiri, sehingga kerahasiaan terjamin, dan kedua tumpukan dan tumpukan adalah 'mengamankan', meskipun tidak selalu (nol) diinisialisasi.

Anda terlalu banyak membaca pengukuran Anda.


1
Bagian Pembaruan pertanyaan sekarang secara eksplisit merujuk pada jawaban yang menerangi Anda.
thb
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.