보안을 위해 힙이 0으로 초기화되면 왜 스택이 초기화되지 않습니까?


15

데비안 GNU / 리눅스 9 시스템에서 바이너리가 실행될 때

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

왜?

나는 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 표준을 고려하지 않습니다. 호출 할 malloc()때마다 동일한 주소를 반환 할 필요는 없지만이 호출 malloc()은 매번 같은 주소를 반환하기 때문에 힙에있는 메모리가 매번 0으로 표시됩니다.

대조적으로 스택은 0으로 보이지 않았습니다.

GNU / Linux 시스템의 어떤 계층이 관찰 된 동작을 일으키는 지 알 수 없기 때문에 후자의 코드가 시스템에서 무엇을할지 모르겠습니다. 시도해 볼 수 있습니다.

최신 정보

@Kusalananda는 의견에서 관찰했습니다.

가치가있는 것에 대해 가장 최근의 코드는 OpenBSD에서 실행될 때 다른 주소와 (때로는) 초기화되지 않은 (0이 아닌) 데이터를 반환합니다. 이것은 분명히 당신이 리눅스에서 목격하는 행동에 대해 아무 말도하지 않습니다.

내 결과가 OpenBSD의 결과와 다르다는 것은 실제로 흥미 롭습니다. 분명히, 내 실험은 내가 생각한 것처럼 커널 (또는 링커) 보안 프로토콜이 아니라 단순한 구현 아티팩트를 발견했습니다.

이 관점에서, @mosvy, @StephenKitt 및 @AndreasGrapentin의 아래 답변이 내 질문을 해결했다고 믿습니다.

스택 오버 플로우 : malloc이 gcc에서 0으로 값을 초기화하는 이유 도 참조하십시오 . (신용 : @bta).


2
가치가있는 것에 대해 가장 최근의 코드는 OpenBSD에서 실행될 때 다른 주소와 (때로는) 초기화되지 않은 (0이 아닌) 데이터를 반환합니다. 이것은 분명히 당신이 리눅스에서 목격하는 행동에 대해 아무 말도 하지 않습니다 .
Kusalananda

질문의 범위를 변경하지 말고 답변과 의견을 중복시키기 위해 편집하지 마십시오. C에서 "힙"은 malloc () 및 calloc ()에 의해 반환 된 메모리 외에는 아무것도 아니며 후자 만 메모리를 0으로 만듭니다. newC ++ 의 연산자 ( "힙")는 Linux에서 malloc ()의 래퍼 일뿐입니다. 커널은 "힙"이 무엇인지 모르거나 신경 쓰지 않습니다.
mosvy

3
두 번째 예는 glibc에서 malloc 구현의 아티팩트를 노출시키는 것입니다. 8 바이트보다 큰 버퍼로 반복 malloc / free를 수행하면 처음 8 바이트 만 0으로 표시됩니다.
mosvy

@Kusalananda 나는 본다. 내 결과가 OpenBSD의 결과와 다르다는 것이 실제로 흥미 롭습니다. 분명히 당신과 Mosvy는 내 실험에서 내가 생각했던 것처럼 커널 (또는 링커) 보안 프로토콜이 아니라 단순한 구현 아티팩트를 발견하고 있음을 보여주었습니다.
thb

@ thb 나는 이것이 올바른 관찰이라고 생각합니다.
Kusalananda

답변:


28

malloc ()에 의해 리턴 된 저장 영역은 0으로 초기화 되지 않았습니다 . 절대로 가정하지 마십시오.

테스트 프로그램에서, 그것은 단지 우연이다 : 나는 추측 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

두 번째 예는 mallocglibc 에서 구현 의 아티팩트를 노출시키는 것입니다 . 8 바이트보다 큰 버퍼로 반복 malloc/ 반복 free하면 다음 샘플 코드에서와 같이 처음 8 바이트 만 0으로 표시됩니다.

#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 () 함수에 도달하기 전에 동적 라이브러리에서 생성자를 실행하는 동적 링커에 의해로드됩니다. 데비안 GNU / 리눅스 9 시스템에서, 사전로드 된 라이브러리를 사용하지 않더라도 프로그램에서 main () 함수 전에 malloc () 및 free ()가 두 번 이상 호출됩니다.
mosvy

23

스택이 초기화되는 방법에 관계없이 C 라이브러리는 호출하기 전에 많은 작업을 수행 main하고 스택에 닿기 때문에 원시 스택이 표시되지 않습니다 .

GNU C 라이브러리와, - 64에, 실행 상기 시작 _start의 호출 진입 점, __libc_start_main세트 일까지, 및 호출까지 후자의 끝 main. 그러나를 호출하기 전에 main여러 다른 함수를 호출하여 다양한 데이터 조각을 스택에 씁니다. 스택의 내용은 함수 호출 사이에서 지워지지 않으므로에 들어가면 main스택에 이전 함수 호출의 남은 내용이 포함됩니다.

이것은 스택에서 얻은 결과만을 설명하며 일반적인 접근 방식과 가정에 관한 다른 답변을 참조하십시오.


시간 main()이 불려질 때 초기화 루틴은 메모리가 반환 한 메모리를 수정했을 수 있습니다. malloc()특히 C ++ 라이브러리가 연결되어있는 경우 "힙"이 어떤 것으로 초기화되었다고 가정하는 것은 실제로 매우 나쁜 가정입니다.
Andrew Henle

Mosvy와 함께 귀하의 답변은 내 질문을 해결합니다. 불행히도이 시스템 은 두 가지 중 하나만 받아 들일 수 있습니다 . 그렇지 않으면 둘 다 받아들입니다.
thb

18

두 경우 모두 초기화되지 않은 메모리 를 얻게 되며 그 내용에 대해 어떤 가정도 할 수 없습니다.

OS가 프로세스에 새 페이지를 할당해야하는 경우 (스택에 사용 된 영역이든 또는 사용중인 영역이든 malloc()) 다른 프로세스의 데이터가 노출되지 않도록합니다. 그것을 보장하는 일반적인 방법은 0으로 채우는 것입니다 (그러나 페이지 가치를 포함하여 다른 것으로 덮어 쓰는 것도 똑같이 유효합니다 /dev/urandom-실제로 일부 디버깅 malloc()구현은 0과 다른 패턴을 작성하여 실수와 같은 잘못된 가정을 포착합니다).

malloc()이 프로세스에서 이미 사용하고 해제 한 메모리의 요청을 만족시킬 수 있으면 내용이 지워지지 않습니다 (사실 지우기와 관련이 없으며 수행 malloc()할 수 없습니다)-메모리가 맵핑되기 전에 발생해야합니다. 주소 공간). 프로세스 / 프로그램에 의해 이전에 작성된 메모리가있을 수 있습니다 (예 : 이전 main()).

예제 프로그램 malloc()에서이 프로세스에 의해 아직 작성되지 않은 영역 (즉, 새 페이지에서 직접 작성 됨)과 ( main()프로그램의 사전 코드에 의해 작성된) 스택이 표시됩니다. 더 많은 스택을 살펴보면 성장 방향으로 0으로 채워져 있음을 알 수 있습니다.

OS 수준에서 무슨 일이 일어나고 있는지 이해하려면 C 라이브러리 계층을 우회 brk()하고 mmap()대신 시스템 호출을 사용하여 상호 작용하는 것이 좋습니다.


1
일주일 전, 나는 전화 malloc()free()거듭 반복 하면서 다른 실험을 시도했다 . malloc()최근에 해제 된 동일한 스토리지를 재사용 할 필요는 없지만 실험에서는 malloc()그렇게했습니다. 매번 같은 주소를 반환했지만 매번 메모리를 무효화했습니다. 이것은 나에게 흥미 있었다. 추가 실험으로 오늘의 질문이 생겼습니다.
thb

1
@thb, 아마도 충분히 명확하지 않을 것입니다. 대부분의 구현은 그들이 당신에게 건네주는 메모리로 malloc()전혀 아무것도 하지 않습니다. 이것은 이전에 사용되었거나 새로 할당 된 것이므로 OS에 의해 0입니다. 테스트에서 후자를 얻었을 것입니다. 마찬가지로, 스택 메모리는 클리어 된 상태로 프로세스에 제공되지만 프로세스가 아직 건드리지 않은 부분을 충분히 볼 수는 없습니다. 스택 메모리 프로세스에 제공되기 전에 지워집니다.
Toby Speight

2
@TobySpeight : brk 및 sbrk는 mmap에서 사용되지 않습니다. pubs.opengroup.org/onlinepubs/7908799/xsh/brk.html 은 LEGACY가 맨 위에 있다고 말합니다.
Joshua

2
사용 초기화 된 메모리를 필요로하는 경우 calloc(대신 옵션이 될 수 있습니다 memset)
eckes

2
@thb와 Toby : 재미있는 사실 : 커널의 새 페이지는 종종 느리게 할당되며, 단지 쓰기시 복사 된 공유 페이지로 매핑됩니다. 이것은 mmap(MAP_ANONYMOUS)당신이 사용하지 않는 한 발생합니다 MAP_POPULATE. 새로운 스택 페이지는 새로운 물리적 페이지에 의해 백업되고 커밍 할 때 유선으로 연결됩니다 (하드웨어 페이지 테이블과 커널의 포인터 / 길이 매핑 목록). 일반적으로 처음으로 만질 때 새로운 스택 메모리가 작성되므로 . 그러나 그렇습니다. 커널은 어떻게 든 데이터 유출을 피해야하며 제로화는 가장 저렴하고 유용합니다.
Peter Cordes

9

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

'보안'이라고 설명하는 것은 실제로 기밀성입니다 . 즉, 프로세스간에 메모리가 명시 적으로 공유되지 않으면 프로세스가 다른 프로세스 메모리를 읽을 수 없습니다. 운영 체제에서 이는 동시 활동 또는 프로세스 격리 의 한 측면입니다 .

운영 체제가이 격리를 보장하기 위해 수행하는 작업은 힙 또는 스택 할당 프로세스에서 메모리가 요청 될 때마다 0으로 채워지는 실제 메모리의 영역에서 가져 오거나 같은 과정 에서 나옵니다 .

당신이 오직 비밀이 보장되도록, 제로, 또는 자신의 쓰레기를보고하고, 그이 보장하지만 모두 반드시 (제로)가 초기화이기는하지만 스택이는 '보안'.

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


1
질문의 업데이트 섹션은 이제 귀하의 답변을 명확하게 참조합니다.
thb
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.