안전상의 이유로 힙이 0으로 초기화된 경우 스택이 초기화되지 않은 이유는 무엇입니까?

안전상의 이유로 힙이 0으로 초기화된 경우 스택이 초기화되지 않은 이유는 무엇입니까?

내 Debian GNU/Linux 9 시스템에서 바이너리를 실행할 때,

  • 스택이 초기화되지 않았지만
  • 힙은 0으로 초기화됩니다.

왜?

제로 초기화가 안전성을 향상시킨다고 생각합니다. 하지만 힙에 대한 것이라면 스택에도 안 되는 이유는 무엇입니까? 스택에도 보안이 필요하지 않나요?

내가 아는 한, 내 문제는 데비안에만 국한된 것이 아닙니다.

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 표준이 포함되지 않습니다. to 호출은 malloc()매번 동일한 주소를 반환할 필요는 없지만 호출은 malloc()매번 동일한 주소를 반환하므로 주목해야 할 점이 흥미롭습니다.힙의 메모리는 매번 지워집니다.

대조적으로, 스택은 아직 0으로 설정되지 않은 것 같습니다.

나는 GNU/Linux 시스템의 어떤 계층이 관찰된 동작을 유발하는지 모르기 때문에 뒤에 있는 코드가 귀하의 컴퓨터에서 무엇을 할 것인지 모릅니다. 시도해 볼 수 있습니다.

고쳐 쓰다

@Kusalananda는 댓글에서 다음과 같이 관찰했습니다.

그럼에도 불구하고 최근 코드는 OpenBSD에서 실행될 때 다른 주소와 (가끔) 초기화되지 않은(0이 아닌) 데이터를 반환합니다. 분명히 이것은 Linux에서 볼 수 있는 동작을 설명하지 않습니다.

내 결과가 OpenBSD의 결과와 다르다는 것은 정말 흥미롭습니다. 분명히 내 실험에서 발견한 것은 내 생각처럼 커널(또는 링커) 보안 프로토콜이 아니라 단지 구현 아티팩트일 뿐이라는 것이었습니다.

이를 염두에 두고 @mosvy, @StephenKitt 및 @AndreasGrapentin의 다음 답변이 내 문제를 종합적으로 해결했다고 믿습니다.

스택 오버플로도 참조하세요.gcc의 malloc이 값을 0으로 초기화하는 이유는 무엇입니까?(제공: @bta).

답변1

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

malloc두 번째 예는 glibc에서 구현의 아티팩트를 노출합니다 . 8바이트보다 큰 버퍼를 반복/사용하는 경우 malloc다음 예제 코드에서와 같이 처음 8바이트만 0으로 처리되는 것을 분명히 볼 수 있습니다.free

#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 라이브러리는 스택을 호출하기 전에 많은 작업을 수행 main하고 스택에 닿기 때문에 원본 스택을 볼 수 없습니다.

x86-64에서 GNU C 라이브러리를 사용하여 다음에서 실행합니다._시작진입점, 통화__libc_start_mainmain그러나 호출되기 전에 다양한 main데이터가 스택에 기록되도록 하는 여러 다른 함수를 호출합니다. 스택의 내용은 함수 호출 사이에 지워지지 않으므로 를 입력하면 main스택에 이전 함수 호출의 나머지 내용이 포함됩니다.

이것은 스택에서 얻은 결과만 설명합니다. 일반적인 접근 방식 및 가정에 대한 다른 답변을 참조하세요.

답변3

두 경우 모두 당신은 얻을 것이다초기화되지 않은기억은 그 내용에 대해 어떤 가정도 할 수 없습니다.

운영 체제가 프로세스에 새 페이지를 할당해야 하는 경우(스택 또는 사용된 영역에 대해 malloc()) 다른 프로세스의 데이터가 0으로 채워지는 것을 보장하는 일반적인 방법이 보장됩니다. 그러나 다른 것으로 덮어쓰는 것도 잘 작동합니다. 심지어 페이지의 내용도 마찬가지입니다. /dev/urandom실제로 일부 디버그 malloc()구현에서는 잘못된 가정을 잡기 위해 0이 아닌 패턴을 작성합니다.

프로세스에 의해 사용 및 해제된 메모리에 대한 요청이 만족 되면 malloc()해당 내용은 지워지지 않습니다(사실 지우기는 이와 관련이 없으며 그럴 malloc()수도 없습니다. 메모리가 매핑되기 전에 발생해야 합니다). 귀하의 주소 공간에). 이전에 프로세스/프로그램에 의해 작성된 메모리를 얻을 수 있습니다(예: before main()).

예제 프로그램에서는 malloc()이 프로세스에 의해 기록되지 않은 영역(즉, 새 페이지에서 직접 가져옴)과 기록된 스택( main()프로그램의 사전 코딩을 통해)을 볼 수 있습니다. 스택을 더 자세히 살펴보면 아래쪽(증가하는 방향)이 0으로 채워져 있음을 알 수 있습니다.

운영 체제 수준에서 무슨 일이 일어나고 있는지 정말로 이해하고 싶다면 C 라이브러리 계층을 우회하고 시스템 호출(예: 및 )을 사용하여 brk()상호 작용하는 것이 좋습니다 mmap().

답변4

당신의 전제가 잘못되었습니다.

당신이 말하는 '안전'은 정말비밀의이는 메모리가 해당 프로세스 간에 명시적으로 공유되지 않는 한 어떤 프로세스도 다른 프로세스의 메모리를 읽을 수 없음을 의미합니다. 운영 체제에서는 다음과 같습니다.격리동시 활동 또는 프로세스.

이러한 격리를 보장하기 위해 운영 체제가 수행하는 작업은 프로세스가 힙 또는 스택 할당을 위해 메모리를 요청할 때마다 해당 메모리가 0으로 채워진 실제 메모리 영역이나 가비지로 채워진 영역에서 나오는 것입니다(여기서동일한 프로세스.

이는 귀하가 제로 또는 자신의 쓰레기만 볼 수 있도록 보장함으로써 기밀성을 보장합니다.둘 다더미그리고스택은 "안전"하지만 반드시 (0) 초기화될 필요는 없습니다.

측정값을 너무 많이 읽고 있습니다.

관련 정보