Nếu heap không được khởi tạo để bảo mật, thì tại sao stack chỉ đơn thuần là chưa được khởi tạo?


15

Trên hệ thống Debian GNU / Linux 9 của tôi, khi tệp nhị phân được thực thi,

  • ngăn xếp chưa được khởi tạo nhưng
  • heap là không khởi tạo.

Tại sao?

Tôi giả định rằng việc khởi tạo bằng không thúc đẩy bảo mật nhưng, nếu cho heap, thì tại sao không cho stack? Có ngăn xếp quá, không cần bảo mật?

Câu hỏi của tôi không dành riêng cho Debian theo như tôi biết.

Mã C mẫu:

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

Đầu ra:

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

Tất nhiên, tiêu chuẩn C không yêu cầu malloc()xóa bộ nhớ trước khi phân bổ nó, nhưng chương trình C của tôi chỉ đơn thuần là để minh họa. Câu hỏi không phải là câu hỏi về C hay về thư viện chuẩn của C. Thay vào đó, câu hỏi là một câu hỏi về lý do tại sao kernel và / hoặc bộ tải thời gian chạy bằng 0 heap mà không phải là stack.

KINH NGHIỆM KHÁC

Câu hỏi của tôi liên quan đến hành vi GNU / Linux có thể quan sát hơn là các yêu cầu của tài liệu tiêu chuẩn. Nếu không chắc ý tôi là gì, thì hãy thử mã này, gọi ra hành vi không xác định thêm ( không xác định, nghĩa là theo tiêu chuẩn C có liên quan) để minh họa điểm:

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

Đầu ra từ máy của tôi:

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

Theo như tiêu chuẩn C có liên quan, hành vi không được xác định, vì vậy câu hỏi của tôi không liên quan đến tiêu chuẩn C. Một cuộc gọi malloc()không cần phải trả lại cùng một địa chỉ mỗi lần, nhưng vì cuộc gọi malloc()này thực sự xảy ra để trả lại cùng một địa chỉ mỗi lần, thật thú vị khi nhận thấy rằng bộ nhớ nằm trong heap, mỗi lần bị xóa.

Ngược lại, ngăn xếp dường như không có gì.

Tôi không biết mã sau sẽ làm gì trên máy của bạn, vì tôi không biết lớp nào của hệ thống GNU / Linux đang gây ra hành vi được quan sát. Bạn có thể nhưng hãy thử nó.

CẬP NHẬT

@Kusalananda đã quan sát trong các ý kiến:

Đối với giá trị của nó, mã gần đây nhất của bạn trả về các địa chỉ khác nhau và dữ liệu (không thường xuyên) không được khởi tạo (khác không) khi chạy trên OpenBSD. Điều này rõ ràng không nói bất cứ điều gì về hành vi mà bạn đang chứng kiến ​​trên Linux.

Kết quả của tôi khác với kết quả trên OpenBSD thực sự thú vị. Rõ ràng, các thí nghiệm của tôi đã phát hiện ra không phải là một giao thức bảo mật kernel (hoặc trình liên kết), như tôi đã nghĩ, mà chỉ là một tạo tác triển khai.

Trong ánh sáng này, tôi tin rằng, cùng nhau, các câu trả lời dưới đây của @mosvy, @StephenKitt và @AndreasGrapentin giải quyết câu hỏi của tôi.

Xem thêm về Stack Overflow: Tại sao malloc khởi tạo các giá trị thành 0 trong gcc? (tín dụng: @bta).


2
Đối với giá trị của nó, mã gần đây nhất của bạn trả về các địa chỉ khác nhau và dữ liệu (không thường xuyên) không được khởi tạo (khác không) khi chạy trên OpenBSD. Điều này rõ ràng không nói bất cứ điều gì về hành vi mà bạn đang chứng kiến ​​trên Linux.
Kusalananda

Vui lòng không thay đổi phạm vi câu hỏi của bạn và đừng cố chỉnh sửa nó để làm cho câu trả lời và nhận xét trở nên dư thừa. Trong C, "heap" không có gì khác ngoài bộ nhớ được trả về bởi malloc () và calloc () và chỉ có cái sau là loại bỏ bộ nhớ; các newtoán tử trong C ++ (còn "đống") là trên Linux chỉ là một wrapper cho malloc (); hạt nhân không biết cũng không quan tâm "heap" là gì.
mosvy

3
Ví dụ thứ hai của bạn chỉ đơn giản là phơi bày một tạo tác của việc triển khai malloc trong glibc; nếu bạn làm điều đó lặp đi lặp lại malloc / free với bộ đệm lớn hơn 8 byte, bạn sẽ thấy rõ rằng chỉ 8 byte đầu tiên là 0.
mosvy

@Kusalananda Tôi thấy. Kết quả của tôi khác với kết quả trên OpenBSD thực sự thú vị. Rõ ràng, bạn và Mosvy đã chỉ ra rằng các thí nghiệm của tôi đã phát hiện ra không phải là một giao thức bảo mật kernel (hoặc trình liên kết), như tôi đã nghĩ, mà chỉ là một tạo tác triển khai.
THB

@thb Tôi tin rằng đây có thể là một quan sát chính xác, đúng vậy.
Kusalananda

Câu trả lời:


28

Bộ nhớ được trả về bởi malloc () không được khởi tạo bằng không. Đừng bao giờ cho rằng nó là.

Trong chương trình thử nghiệm của bạn, đó chỉ là một sự may mắn: Tôi đoán malloc() vừa có một khối mới mmap(), nhưng cũng không nên dựa vào điều đó.

Ví dụ: nếu tôi chạy chương trình của bạn trên máy theo cách này:

$ 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

Ví dụ thứ hai của bạn chỉ đơn giản là phơi bày một tạo tác của việc mallocthực hiện trong glibc; nếu bạn làm điều đó lặp đi lặp lại malloc/free với bộ đệm lớn hơn 8 byte, bạn sẽ thấy rõ rằng chỉ có 8 byte đầu tiên là 0, như trong mã mẫu sau.

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

Đầu ra:

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
Vâng, vâng, nhưng đây là lý do tại sao tôi đã đặt câu hỏi ở đây hơn là trên Stack Overflow. Câu hỏi của tôi không phải là về tiêu chuẩn C mà là về cách các hệ thống GNU / Linux hiện đại thường liên kết và tải nhị phân. LD_PRELOAD của bạn hài hước nhưng trả lời một câu hỏi khác ngoài câu hỏi tôi muốn hỏi.
19:30

19
Tôi rất vui vì tôi đã làm bạn cười, nhưng những giả định và định kiến ​​của bạn không hề buồn cười chút nào. Trên "hệ thống GNU / Linux hiện đại", các nhị phân thường được tải bởi một trình liên kết động, đang chạy các hàm tạo từ các thư viện động trước khi đến hàm main () từ chương trình của bạn. Trên hệ thống Debian GNU / Linux 9 của bạn, cả malloc () và free () sẽ được gọi nhiều lần trước hàm main () từ chương trình của bạn, ngay cả khi không sử dụng bất kỳ thư viện được tải sẵn nào.
mosvy

23

Bất kể cách ngăn xếp được khởi tạo như thế nào, bạn không thấy ngăn xếp nguyên sơ, bởi vì thư viện C thực hiện một số điều trước khi gọi main và chúng chạm vào ngăn xếp.

Với thư viện GNU C, trên x86-64, việc thực thi bắt đầu tại điểm nhập _start , sẽ gọi __libc_start_mainđể thiết lập mọi thứ và sau đó kết thúc cuộc gọi main. Nhưng trước khi gọi main, nó gọi một số hàm khác, khiến các phần dữ liệu khác nhau được ghi vào ngăn xếp. Nội dung của ngăn xếp không bị xóa ở giữa các lệnh gọi hàm, vì vậy khi bạn vàomain , ngăn xếp của bạn chứa phần còn lại từ các lệnh gọi hàm trước đó.

Điều này chỉ giải thích kết quả bạn nhận được từ ngăn xếp, xem các câu trả lời khác liên quan đến phương pháp tiếp cận và giả định chung của bạn.


Lưu ý rằng theo thời gian main()được gọi, các thói quen khởi tạo rất có thể đã sửa đổi bộ nhớ được trả về bởi malloc()- đặc biệt là nếu các thư viện C ++ được liên kết. Giả sử "heap" được khởi tạo cho bất cứ điều gì là một giả định thực sự rất tồi tệ.
Andrew Henle

Câu trả lời của bạn cùng với Mosvy giải quyết câu hỏi của tôi. Hệ thống không may cho phép tôi chỉ chấp nhận một trong hai; nếu không, tôi sẽ chấp nhận cả hai.
26 phút

18

Trong cả hai trường hợp, bạn nhận được bộ nhớ chưa được khởi tạo và bạn không thể đưa ra bất kỳ giả định nào về nội dung của nó.

Khi HĐH phải phân bổ một trang mới cho quy trình của bạn (cho dù đó là cho ngăn xếp của nó hay cho đấu trường được sử dụng malloc()), nó sẽ đảm bảo rằng nó sẽ không lộ dữ liệu từ các quy trình khác; cách thông thường để đảm bảo rằng điền vào nó bằng số không (nhưng nó có giá trị như nhau để ghi đè lên bất cứ thứ gì khác, kể cả một trang có giá trị /dev/urandom- thực tế là một số gỡ lỗimalloc() triển khai viết các mẫu khác không, để bắt các giả định sai lầm như của bạn).

Nếu quy trình này malloc()có thể đáp ứng yêu cầu từ bộ nhớ đã được sử dụng và giải phóng, thì nội dung của nó sẽ không bị xóa (thực tế, việc xóa không liên quan gì malloc()và không thể - nó phải xảy ra trước khi bộ nhớ được ánh xạ vào không gian địa chỉ của bạn). Bạn có thể nhận được bộ nhớ đã được ghi trước đây bởi quy trình / chương trình của bạn (ví dụ: trước đómain() ).

Trong chương trình ví dụ của bạn, bạn đang thấy một malloc()khu vực chưa được viết bởi quy trình này (tức là trực tiếp từ một trang mới) và một ngăn xếp đã được ghi vào (bằng main()mã trước trong chương trình của bạn). Nếu bạn kiểm tra nhiều ngăn xếp hơn, bạn sẽ thấy nó không đầy hơn nữa (theo hướng tăng trưởng của nó).

Nếu bạn thực sự muốn hiểu những gì đang xảy ra ở cấp độ HĐH, tôi khuyên bạn nên bỏ qua lớp Thư viện C và tương tác bằng các cuộc gọi hệ thống như brk()mmap()thay vào đó.


1
Một hoặc hai tuần trước, tôi đã thử một thử nghiệm khác, gọi malloc()free()lặp lại. Mặc dù không có gì yêu cầu malloc()sử dụng lại cùng một bộ lưu trữ được giải phóng gần đây, nhưng trong thử nghiệm, malloc()đã xảy ra để làm điều đó. Nó tình cờ trả lại cùng một địa chỉ mỗi lần, nhưng cũng vô hiệu hóa bộ nhớ mỗi lần, điều mà tôi không mong đợi. Điều này thật thú vị với tôi. Các thí nghiệm tiếp theo đã dẫn đến câu hỏi ngày nay.
THB

1
@thb, Có lẽ tôi không đủ rõ ràng - hầu hết các triển khai malloc()hoàn toàn không làm với bộ nhớ mà chúng trao cho bạn - nó được sử dụng trước đó hoặc được gán mới (và do hệ điều hành không sử dụng). Trong thử nghiệm của bạn, bạn rõ ràng có cái sau. Tương tự, bộ nhớ ngăn xếp được cung cấp cho quy trình của bạn ở trạng thái bị xóa, nhưng bạn không kiểm tra nó đủ xa để xem các phần mà quy trình của bạn chưa chạm vào. Bộ nhớ ngăn xếp của bạn sẽ bị xóa trước khi đưa vào quy trình của bạn.
Toby Speight

2
@TobySpeight: brk và sbrk bị lỗi thời bởi mmap. pubs.opengroup.org/onlinepub/7908799/xsh/brk.html nói LEGACY ngay trên đầu trang.
Joshua

2
Nếu bạn cần bộ nhớ khởi tạo bằng cách sử dụng calloccó thể là một tùy chọn (thay vì memset)
eckes

2
@thb và Toby: sự thật thú vị: các trang mới từ kernel thường được phân bổ một cách lười biếng và chỉ đơn thuần là sao chép trên bản đồ được ánh xạ đến một trang zeroed được chia sẻ. Điều này xảy ra mmap(MAP_ANONYMOUS)trừ khi bạn sử dụng MAP_POPULATElà tốt. Các trang ngăn xếp mới hy vọng được hỗ trợ bởi các trang vật lý mới và được nối dây (được ánh xạ trong các bảng trang phần cứng, cũng như danh sách ánh xạ con trỏ / chiều dài của hạt nhân) khi phát triển, vì thông thường bộ nhớ ngăn xếp mới được ghi khi chạm vào lần đầu tiên . Nhưng có, hạt nhân phải tránh rò rỉ dữ liệu bằng cách nào đó, và zeroing là rẻ nhất và hữu ích nhất.
Peter Cordes

9

Tiền đề của bạn là sai.

Những gì bạn mô tả là "bảo mật" là thực sự bảo mật , có nghĩa là không có quá trình nào có thể đọc được bộ nhớ xử lý khác, trừ khi bộ nhớ này được chia sẻ rõ ràng giữa các quy trình này. Trong một hệ điều hành, đây là một khía cạnh của sự cô lập các hoạt động hoặc quy trình đồng thời.

Hệ điều hành đang làm gì để đảm bảo sự cô lập này, là bất cứ khi nào bộ nhớ được yêu cầu bởi quá trình phân bổ heap hoặc stack, bộ nhớ này hoặc đến từ một vùng trong bộ nhớ vật lý chứa đầy các số 0 hoặc chứa đầy rác. đến từ cùng một quá trình .

Điều này đảm bảo rằng bạn chỉ nhìn thấy số không, hoặc rác của riêng bạn, do đó tính bảo mật được đảm bảo và cả đống ngăn xếp đều 'an toàn', mặc dù không nhất thiết (không bắt đầu).

Bạn đang đọc quá nhiều vào số đo của bạn.


1
Phần Cập nhật của câu hỏi hiện tham chiếu rõ ràng câu trả lời chiếu sáng của bạn.
THB
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.