내 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_main
main
그러나 호출되기 전에 다양한 main
데이터가 스택에 기록되도록 하는 여러 다른 함수를 호출합니다. 스택의 내용은 함수 호출 사이에 지워지지 않으므로 를 입력하면 main
스택에 이전 함수 호출의 나머지 내용이 포함됩니다.
이것은 스택에서 얻은 결과만 설명합니다. 일반적인 접근 방식 및 가정에 대한 다른 답변을 참조하세요.
답변3
두 경우 모두 당신은 얻을 것이다초기화되지 않은기억은 그 내용에 대해 어떤 가정도 할 수 없습니다.
운영 체제가 프로세스에 새 페이지를 할당해야 하는 경우(스택 또는 사용된 영역에 대해 malloc()
) 다른 프로세스의 데이터가 0으로 채워지는 것을 보장하는 일반적인 방법이 보장됩니다. 그러나 다른 것으로 덮어쓰는 것도 잘 작동합니다. 심지어 페이지의 내용도 마찬가지입니다. /dev/urandom
실제로 일부 디버그 malloc()
구현에서는 잘못된 가정을 잡기 위해 0이 아닌 패턴을 작성합니다.
프로세스에 의해 사용 및 해제된 메모리에 대한 요청이 만족 되면 malloc()
해당 내용은 지워지지 않습니다(사실 지우기는 이와 관련이 없으며 그럴 malloc()
수도 없습니다. 메모리가 매핑되기 전에 발생해야 합니다). 귀하의 주소 공간에). 이전에 프로세스/프로그램에 의해 작성된 메모리를 얻을 수 있습니다(예: before main()
).
예제 프로그램에서는 malloc()
이 프로세스에 의해 기록되지 않은 영역(즉, 새 페이지에서 직접 가져옴)과 기록된 스택( main()
프로그램의 사전 코딩을 통해)을 볼 수 있습니다. 스택을 더 자세히 살펴보면 아래쪽(증가하는 방향)이 0으로 채워져 있음을 알 수 있습니다.
운영 체제 수준에서 무슨 일이 일어나고 있는지 정말로 이해하고 싶다면 C 라이브러리 계층을 우회하고 시스템 호출(예: 및 )을 사용하여 brk()
상호 작용하는 것이 좋습니다 mmap()
.
답변4
당신의 전제가 잘못되었습니다.
당신이 말하는 '안전'은 정말비밀의이는 메모리가 해당 프로세스 간에 명시적으로 공유되지 않는 한 어떤 프로세스도 다른 프로세스의 메모리를 읽을 수 없음을 의미합니다. 운영 체제에서는 다음과 같습니다.격리동시 활동 또는 프로세스.
이러한 격리를 보장하기 위해 운영 체제가 수행하는 작업은 프로세스가 힙 또는 스택 할당을 위해 메모리를 요청할 때마다 해당 메모리가 0으로 채워진 실제 메모리 영역이나 가비지로 채워진 영역에서 나오는 것입니다(여기서동일한 프로세스.
이는 귀하가 제로 또는 자신의 쓰레기만 볼 수 있도록 보장함으로써 기밀성을 보장합니다.둘 다더미그리고스택은 "안전"하지만 반드시 (0) 초기화될 필요는 없습니다.
측정값을 너무 많이 읽고 있습니다.