Computer Science/컴퓨터 구조
[CSAPP] 3.10 Combining Control and Data in Machine-Level Programs(기계 수준 프로그램에서 제어와 데이터 결합)
tgool
2023. 1. 24. 20:18
3.10 Combining Control and Data in Machine-Level Programs(기계 수준 프로그램에서 제어와 데이터 결합)
- C언어에서 가장 중요한 개념인 포인터의 개념에 대해 알아본다.
- 기계어 프로그램의 상세한 동작을 디버거 GDB를 통해 알아본다.
- 버퍼 오버플로우가 현실 세계 시스템에서 보안 취약성을 가져오는 것에 대해 알아본다.
- 함수에 의해 요구된 스택 저장소의 양이 변화하는 경우에 대해 머신레벨 단계에서 어떻게 수행되는지 알아본다.
3.10.1 Understanding Pointers(포인터의 이해)
- 포인터는 다른 자료구조 내 원소들을 참조하는 통일된 방법을 수행하는 역할을 한다.
- 포인터는 연관된 자료형을 가진다. (포괄적인 void 타입 포인터는 묵시적 형변환 또는 명시적 형변환을 통해 이용될 수 있다.)
- 포인터 타입은 기계어 부분이 아니고 C에서 프로그래머들을 위해 추상화한 개념이다.
- 모든 포인터는 특정한 값을 가진다. 그 값은 지정된 타입의 객체의 주소값이다. (NULL값은 포인터가 지칭하고 있지 않은 상태임을 의미)
- 포인터는 ‘&’연산자를 이용해서 생성한다. 이에 매칭되는 기계어는 메모리 참조 주소값을 계산하는 leaq가 있다.
- 포인터는 ‘*’연산자를 통해 dereferenced(역참조;메모리 참조) 된다.
- 배열과 포인터는 밀접한 연관이 있다. 배열의 이름은 마치 포인터 변수처럼 참조 될 수 있다. ex) a[3] == *(a+3), 포인터 p에서 p + i로 주소값 이동 가능. 여기서 i값은 데이터 타입의 크기
- 포인터의 형변환은 타입만 바뀌고 값은 변화하지 않는다. 또한, 캐스팅이 수연산 보다 먼저 실행된다. ex) (int *) p+7 computes p + 28, vs (int *) (p+7) computes p + 7.
- 포인터는 함수를 가리킬 수도 있다.
3.10.2 Life in the Real World: Using the gdb Debugger(실제 적용하기: GDB 디버거 사용)
- GNU(운영체제의 하나이고 오픈소스) 디버거인 GDB는 기계어 프로그램의 분석 및 런타임 평가 등을 지원하는 유용한 기능을 제공
- GDB를 사용하면, 프로그램의 실행 과정을 정교하게 제어하면서 관찰 및 분석을 할 수 있다.
- 프로그램 실행 중 브레이크 포인트가 동작하면 프로그램 실행은 중단되고 제어를 사용자에게 넘긴다.
- GDB는 command line interface로 모호한 command syntax을 가졌기 때문에 help command나 온라인 문서를 통해 command를 확인하거나 GUI인 DDD 디버거를 사용한다.
3.10.3 Out-of-Bounds Memory References and Buffer Overflow(범위를 벗어난 메모리 참조와 버퍼 오버플로우)
C에서는 배열 참조시 범위를 체크하지 않고 지역변수들은 스택에 리턴 주소 같은 상태정보와 함께 저장된다.
- 만약 배열에 문자열이 저장될 때 return address(반환 주소;함수가 끝나고 돌아갈 이전 함수의 주소) 범위까지 침범해서 저장되면 심각한 실행 오류가 일어난다.
- buffer overflow가 배열의 정해진 범위를 벗어나서 발생하는 대표적인 예시이다. ex) scanf, gets, strcpy, strcats 등 같이 버퍼를 다루면서 길이를 입력하지 않는 함수는 안전성이 보장되지 않는다.
- 이런 버퍼 오버플로우는 컴퓨터 네트워크의 보안을 공격하는 가장 흔한 방법이다.
- 공격자는 exploit code를 return address를 덮어 씌워서 공격자에게 운영체제 기능 및 권한을 부여하거나 승인되지 않은 작업을 수행해서 공격하는 방식이다.
3.10.4 Thwarting Buffer Overflow Attacks(버퍼 오버플로우 공격 대응 기법)
Stack Randomization
- 공격자는 exploit code를 시스템에 삽입하기 위해 공격 스트링 내에 코드 뿐만 아니라 코드로의 포인터까지 삽입해야 한다. 이를 위해서 공격자는 스트링이 위치하게 될 스택의 주소를 알아야한다. 과거에는 이 스택의 위치를 예측하기 쉬웠다.
- 스택 랜덤화는 스택의 위치를 프로그램의 매 실행마다 다르게 해주는 것
- 스택 랜덤화의 원리는 할당 함수인 allocate로 사용되는 않는 stack 공간을 n개 생성해주어서 stack의 위치를 프로그램 실행마다 다르게 해주는 것이다. (여기서 n은 충분히 크고(위치를 충분히 바꿀 수 있고) 충분히 작아야(메모리 낭비를 줄이기 위해)한다.
- 하지만 이 방법은 공격자가 반복적으로 주소를 바꿔가며 무수히 많은 공격을 시도하면 랜덤화를 뚫을 수 있다. ex) 프로그램 카운터만을 다음 명령을 증가 시키는 nop이라는 코드를 이용해서 계속 연속적인 접근
Stack Corruption Detection(스택 손상 검출)
- 스택이 손상되는 것을 감지하는 기법
- GCC 최신 버전에서는 stack protector(스택 보호기)라 불리는 스택 손상 검출 매커니즘을 제공한다.
- 상단 그림처럼 스택 보호기는 함수 스택 구성에 ‘Canary’ 값을 배열 buf와 return address 값 사이에 위치시켜 Canary 값을 통해 스택이 손상 되었는지 체크한다.
- canary 값은 프로그램 실행마다 랜덤으로 바뀌며 buffer overflow 등을 통해 sfp/ret에 접근하려 하면 canary가 변경되어 경고가 발생합니다.
Limiting Executable Code Regions(실행코드 영역 제한)
- 공격자가 실행코드를 프로그램에 추가할 가능성을 제거하는 방법이다.
- read, write, execute 실행 권한을 제한하여 실행코드 영역을 제한하여 실행코드를 프로그램에 추가할 수 없게 한다.
3.10.5 Supporting Variable-Size Stack Frames (가변 크기 스택 프래임 지원)
- allocate 동적할당, 라이브러리 함수를 통해 스택에 가변 지역저장공간을 만들 수 있다.
- 이때, 가변 크기 스택 프래임이 이용된다.
- 동적할당 사용 후 deallocate를 해줘야 한다. (메모리 공간 절약)