2011-01-01 2 views
13

그래서 Linux는 x86 프로세서 (커널 코드, 커널 데이터, 사용자 코드, 사용자 데이터)에 대해 4 개의 기본 세그먼트를 사용하지만 모두 동일한 기본 및 한계 (0x00000000 및 0xfffff)를 가지고 있습니다. 즉, 각 세그먼트가 동일한 일련의 선형 주소x86에서 Linux는 사용자 프로세스와 커널에 다른 세그먼트를 사용하는 이유는 무엇입니까?

사용자/커널 세그먼트가있는 이유는 무엇입니까? 왜 코드와 데이터를위한 별도의 세그먼트 (x86 프로세서가 cs 및 ds 레지스터를 다루는 지에 대한 이유)가 있어야하지만 왜 단일 코드 세그먼트와 단일 데이터 세그먼트가 없는지 이해합니다. 메모리 보호는 페이징을 통해 이루어지며 사용자와 커널 세그먼트는 어쨌든 동일한 선형 주소로 매핑됩니다.

답변

11

x86 아키텍처의 동료 유형과 각 세그먼트의 기술자로 권한 수준 :

은 에드를 참조하십시오. 기술 어의 유형은 세그먼트를 읽기 전용, 읽기/쓰기, 실행 가능하게 만들 수 있지만 동일한 기본 및 제한이있는 여러 세그먼트의 주된 이유는 다른 설명자 권한 수준 (DPL)을 사용할 수있게하는 것입니다.

DPL은 2 비트이며 0에서 3까지의 값을 인코딩 할 수 있습니다. 특권 레벨이 0 일 때 가장 높은 특권을 가진 ring 0이라고합니다. 리눅스 커널을위한 세그먼트 디스크립터는 링 0이며, 사용자 공간을위한 세그먼트 디스크립터는 링 3 (최소 특권)이다. 이것은 대부분의 세그먼트 화 된 운영 체제에 해당됩니다. 운영 체제의 핵심은 링 0이고 나머지는, 4 개 개의 세그먼트 언급 한 바와 같이

리눅스 커널이 설정 링 3입니다 :

  • __KERNEL_CS (커널 코드 세그먼트, 기본 = 0, 제한 = 4GB의 TYPE = 10, DPL = 0)
  • __KERNEL_DS (커널 데이터 세그먼트,베이스 = 0, 제한 = 4GB의 타입 = 2, DPL = 0)
  • __USER_CS (사용자 코드 세그먼트는,베이스 = 0 한도 = 4GB, 유형 = 10, DPL = 3)
  • __USER_DS (사용자 데이터 세그먼트, 기본 = 0, 제한 = 4GB, 유형 = 2, DPL = 3)

모든 기본과 제한은 동일하지만 커널 세그먼트는 DPL 0, 사용자 세그먼트는 DPL 3, 코드 세그먼트는 실행 가능하고 읽을 수 있으며 (쓰기 가능하지 않음) 데이터 세그먼트는 읽기 쉽고 쓰기 가능 (실행 불가능).

참조 : X86에

+0

이 좋아, 그것은 보인다 포함되어 /의 FS를 gs와 관련 커널 파일 어쨌든 사용자로서 어떤 선형 주소에 액세스 할 수있는 것처럼, 왜 커널을위한 여분의 세그먼트가 있습니까? 사용자로서 메모리 주소 x에 액세스하려는 경우 x 오프셋과 함께 사용자 데이터 세그먼트 만 사용합니다. 커널은 오프셋 x가있는 커널 데이터 세그먼트를 사용할 수 있지만 동일한 선형 주소, 즉 실제 메모리의 동일한 주소로 매핑되므로 어떤 보호 기능을 제공합니까? –

+1

@anjruu : 일부 어셈블리 지침에는 특정 권한 수준이 필요합니다. 그렇지 않으면 일반 보호 (GP) 오류가 발생합니다. 예를 들어, 포트로부터 바이트를 읽는 'IN' 명령은 현재 PL (CPL)이 입출력 PL (IOPL;'FLAGS'레지스터의 비트 12와 13)보다 작거나 같아야하며, Linux의 경우 0입니다. CPL은 'CS'(코드 세그먼트) 레지스터에 해당하는 세그먼트 설명 자의 DPL입니다. –

+0

@Daniel : Gotcha, 맞아. 감사! –

-1

사용자 공간에서 실행되는 프로그램에서 커널 메모리를 읽을 수 없어야합니다.

프로그램 데이터가 종종 실행 가능하지 않습니다 (DEP, 오버플로 된 버퍼 및 기타 악의적 인 공격을 실행하는 것을 방지하는 프로세서 기능).

다른 모든 세그먼트는 다른 권한을 가지고 있습니다. 그래서 잘못된 세그먼트에 액세스하면 "세그먼트 오류"가 발생합니다.

2

x86 메모리 관리 아키텍처는 세그먼트 화와 페이징을 모두 사용합니다. 매우 대략적으로 말하면 세그먼트는 자체 보호 정책이있는 프로세스 주소 공간의 파티션입니다. 따라서 x86 아키텍처에서는 프로세스가 여러 개의 인접한 세그먼트로 나누는 메모리 주소 범위를 분할하고 각각에 서로 다른 보호 모드를 할당 할 수 있습니다. 페이징은 프로세스의 주소 공간 중 작은 영역 (대개 4KB)을 실제 실제 메모리의 덩어리로 매핑하는 기술입니다. 따라서 페이징은 세그먼트 내의 영역이 실제 RAM에 매핑되는 방법을 제어합니다.

  1. 하나의 세그먼트 (주소 0xBFFFFFFF 통해 0x00000000의) 사용자 수준, 프로세스 관련 데이터 등의 프로그램의 코드, 정적 데이터, 힙, 및 스택으로서 :

    모든 프로세스는 두 개의 세그먼트를 갖는다. 모든 프로세스에는 독자적인 사용자 세그먼트가 있습니다.

  2. 커널 명령어, 데이터, 커널 코드가 실행될 수있는 일부 스택과 같은 커널 특정 데이터를 포함하는 하나의 세그먼트 (주소 0xC0000000에서 0xFFFFFFFF)이며이 세그먼트의 영역은 물리적 메모리를 사용하므로 커널이 주소 변환에 대해 걱정할 필요없이 물리적 메모리 위치에 직접 액세스 할 수 있습니다. 동일한 커널 세그먼트가 모든 프로세스에 매핑되지만 프로세스는 보호 된 커널 모드에서 실행될 때만 액세스 할 수 있습니다.

따라서 사용자 모드에서 프로세스는 0xC0000000 미만의 주소에만 액세스 할 수 있습니다. 이 주소보다 높은 주소에 액세스하면 오류가 발생합니다. 그러나 사용자 모드 프로세스가 커널에서 실행되기 시작하면 (예 : 시스템 호출을 한 후) CPU의 보호 비트가 감독자 모드로 변경되고 일부 분할 레지스터가 변경됩니다. 즉 프로세스가 0xC0000000 이상의 주소에 액세스 할 수 있습니다. HERE

0

- 세그먼트 레지스터는 버퍼 오버 플로우 검사에 사용되는 리눅스 [아래의 코드를 참조 stac에서 일부 char 배열을 정의한 k] :

static void 
printint(int xx, int base, int sgn) 
{ 
    char digits[] = "ABCDEF"; 
    char buf[16]; 
    int i, neg; 
    uint x; 

    neg = 0; 
    if(sgn && xx < 0){ 
     neg = 1; 
     x = -xx; 
    } else { 
     x = xx; 
    } 

    i = 0; 
    do{ 
     buf[i++] = digits[x % base]; 
    }while((x /= base) != 0); 
    if(neg) 
     buf[i++] = '-'; 

    while(--i >= 0) 
     my_putc(buf[i]); 
} 

이제 코드 gcc 생성 코드의 디스 어셈블리가 표시됩니다. 기능 printint에 대한 어셈블러 코드의

덤프 : 우리는이 함수에서 스택 기반의 문자 배열을 제거하면

0x00000000004005a6 <+0>: push %rbp 
    0x00000000004005a7 <+1>: mov %rsp,%rbp 
    0x00000000004005aa <+4>: sub $0x50,%rsp 
    0x00000000004005ae <+8>: mov %edi,-0x44(%rbp) 


    0x00000000004005b1 <+11>: mov %esi,-0x48(%rbp) 
    0x00000000004005b4 <+14>: mov %edx,-0x4c(%rbp) 
    0x00000000004005b7 <+17>: mov %fs:0x28,%rax ------> obtaining an 8 byte guard from based on a fixed offset from fs segment register [from the descriptor base in the corresponding gdt entry] 
    0x00000000004005c0 <+26>: mov %rax,-0x8(%rbp) -----> pushing it as the first local variable on to stack 
    0x00000000004005c4 <+30>: xor %eax,%eax 
    0x00000000004005c6 <+32>: movl $0x33323130,-0x20(%rbp) 
    0x00000000004005cd <+39>: movl $0x37363534,-0x1c(%rbp) 
    0x00000000004005d4 <+46>: movl $0x42413938,-0x18(%rbp) 
    0x00000000004005db <+53>: movl $0x46454443,-0x14(%rbp) 

... 
... 
    // function end 

    0x0000000000400686 <+224>: jns 0x40066a <printint+196> 
    0x0000000000400688 <+226>: mov -0x8(%rbp),%rax -------> verifying if the stack was smashed 
    0x000000000040068c <+230>: xor %fs:0x28,%rax --> checking the value on stack is matching the original one based on fs 
    0x0000000000400695 <+239>: je  0x40069c <printint+246> 
    0x0000000000400697 <+241>: callq 0x400460 <[email protected]> 
    0x000000000040069c <+246>: leaveq 
    0x000000000040069d <+247>: retq 

지금, GCC는이 가드 검사를 생성하지 않습니다.

커널 모듈의 경우에도 gcc가 생성 한 것과 같은 것을 보았습니다. 기본적으로 일부 커널 코드를 botrapping하는 동안 충돌이 발생했으며 가상 주소 0x28로 오류가 발생했습니다. 나중에 나는 스택 포인터를 올바르게 초기화했고 프로그램을 올바르게로드했다는 생각을했습니다. gdt에 올바른 항목이 없기 때문에 fs 기반 오프셋을 유효한 가상 주소로 변환합니다.

그러나 커널 코드의 경우 단순히 __stack_chk_fail @ plt>와 같은 점프 대신 오류가 무시되었습니다.

gcc에이 가드를 추가하는 관련 컴파일러 옵션은 -fstack-protector입니다. 나는 이것이 사용자 app을 컴파일하는 기본적으로 가능하다고 생각한다.

커널의 경우 config CC_STACKPROTECTOR 옵션을 통해이 gcc 플래그를 활성화 할 수 있습니다.

 
config CC_STACKPROTECTOR 
699  bool "Enable -fstack-protector buffer overflow detection (EXPERIMENTAL)" 
700  depends on SUPERH32 
701  help 
702   This option turns on the -fstack-protector GCC feature. This 
703   feature puts, at the beginning of functions, a canary value on 
704   the stack just before the return address, and validates 
705   the value just before actually returning. Stack based buffer 
706   overflows (that need to overwrite this return address) now also 
707   overwrite the canary, which gets detected and the attack is then 
708   neutralized via a kernel panic. 
709 
710   This feature requires gcc version 4.2 or above. 

이는 DPL 각 세그먼트에 대한 최소한의 보안 수준을 설정하지만, 그래서 리눅스/아치/86 /이 /가 ASM/stackprotector.h

관련 문제