다른 명령
새 문서: 분류: 소프트웨어 기반 보안 = 개요 = '''AddressSanitizer''' ('''ASan''')는 C/C++ 프로그램에서 발생하는 메모리 오류를 탐지하기 위한 메모리 오류 검사 도구이다. AddressSanitizer는 다음과 같은 메모리 오류를 탐지할 수 있다: * Use-after-free * Heap buffer overflow * Stack buffer overflow * Global buffer overflow * Use-after-return * Use-after-scope * Initialization order bugs * Memory leaks == 작동 원리 == ===... |
편집 요약 없음 |
||
12번째 줄: | 12번째 줄: | ||
* Initialization order bugs | * Initialization order bugs | ||
* Memory leaks | * Memory leaks | ||
== 작동 원리 == | == 작동 원리 == |
2025년 5월 28일 (수) 01:57 기준 최신판
개요
AddressSanitizer (ASan)는 C/C++ 프로그램에서 발생하는 메모리 오류를 탐지하기 위한 메모리 오류 검사 도구이다. AddressSanitizer는 다음과 같은 메모리 오류를 탐지할 수 있다:
- Use-after-free
- Heap buffer overflow
- Stack buffer overflow
- Global buffer overflow
- Use-after-return
- Use-after-scope
- Initialization order bugs
- Memory leaks
작동 원리
RedZone & Poisoning
AddressSanitizer는 런타임 라이브러리를 통해 malloc
및 free
함수를 대체한다. 이 라이브러리는 메모리의 할당 및 해제 영역 주위에 "레드존(red zone)"이라 불리는 보호 구역을 추가하며, RedZone영역은 Poisoned 처리되어서, 만약 Application이 Poisoned된 영역에 접근할 경우 에러를 리포트한다.
malloc
호출 시, 할당된 영역 주변에 보호용 바이트(레드존)를 삽입하고 이를 포이즌 처리한다.free
호출 후 해당 메모리 블록은 "격리(quarantine)" 상태로 두고, 여전히 포이즌된 상태를 유지한다.
이는 해제된 메모리에 잘못 접근할 경우 탐지를 가능하게 한다 (Use-after-free 오류).
컴파일러는 프로그램의 모든 메모리 접근을 다음과 같이 변경한다: 변경 전:
*address = ...; // 또는 ... = *address;
변경 후:
if (IsPoisoned(address)) {
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // 또는 ... = *address;
여기서:
IsPoisoned(address)
: 해당 주소가 포이즌된 영역인지 확인한다.ReportError
: 오류가 감지되면 접근 주소, 크기, 쓰기 여부 등의 정보를 포함한 상세 보고를 생성한다.IsPoisoned
함수는 매우 빠르게 작동해야 하며, 런타임 오버헤드를 최소화하는 것이 중요하다.ReportError
함수는 컴팩트하게 구현되어야 하며, 에러 메시지는 디버깅에 유용한 정보를 제공해야 한다.
AddressSanitizer는 프로그램 전체의 메모리 레이아웃과 별도의 "섀도우 메모리"를 활용하여 포이즌 상태를 추적한다.
Shadow Memory
AddressSanitizer는 가상 주소 공간을 두 가지 상호 배타적인 구역으로 나눈다:
- 메인 애플리케이션 메모리 (Mem): 일반적인 프로그램 코드가 사용하는 주 메모리 공간이다.
- 섀도우 메모리 (Shadow): 메모리 접근 상태를 추적하기 위한 메타데이터를 저장한다. 이 메모리는 메인 메모리의 각 바이트에 대한 정보를 대응하여 기록한다.
섀도우 메모리는 포이즌 상태(유효하지 않음)를 나타내기 위해 특수한 값을 저장한다. 메인 메모리의 특정 바이트를 포이즌 처리한다는 것은, 해당 바이트에 대응하는 섀도우 메모리 위치에 특수 값을 기록한다는 의미이다. 섀도우 메모리의 핵심 특징은 메인 메모리와의 빠른 주소 변환에 있다. AddressSanitizer는 아래와 같은 방식으로 주소 변환을 수행한다:
shadow_address = MemToShadow(address);
변환 함수 MemToShadow
는 일반적으로 시프트 및 오프셋 연산을 통해 매우 빠르게 수행된다. 예를 들어:
// 예시 변환식: shadow = (mem >> 3) + offset; // 8바이트마다 Shadow Memory영역이 할당되었을 경우.
이는 메모리 접근 시점마다 동적으로 섀도우 주소를 계산할 수 있도록 한다. 이를 통해서 컴파일러는 다음과 같은 코드를 프로그램에 삽입한다.
shadow_address = MemToShadow(address);
if (ShadowIsPoisoned(shadow_address)) {
ReportError(address, kAccessSize, kIsWrite);
}
ShadowIsPoisoned
는 섀도우 메모리 값을 검사하여 해당 접근이 유효한지 판단한다.ReportError
는 포이즌된 메모리 접근을 감지한 경우 오류를 보고한다.
섀도우 메모리 매핑 규칙
AddressSanitizer는 애플리케이션 메모리 8바이트(qword)를 섀도우 메모리의 1바이트로 매핑한다. 이로 인해 섀도우 메모리의 각 바이트는 8바이트 단위의 메모리 접근 가능 상태를 요약해서 표현한다. 정렬된 8바이트(qword)에 대해 가능한 섀도우 값은 총 9가지뿐이다:
- 0 – 해당 qword의 모든 바이트가 접근 가능 (unpoisoned).
- 음수 – 해당 qword의 모든 바이트가 접근 불가능 (fully poisoned).
- 1~7 – 처음 k 바이트는 접근 가능하고, 나머지 (8-k) 바이트는 접근 불가능. 이는 Malloc으로 할당된 메모리 영역은 주로 Sequential하게 할당된다는 사실에 의거한다. 따라서 대부분의 경우 Malloc의 마지막 8바이트만이, 1부터 7의 값을 가진다.
예: malloc(13)
을 호출하면, 다음과 같은 섀도우 상태가 된다:
- 첫 번째 qword: 8바이트 모두 접근 가능 → 섀도우 값: 0
- 두 번째 qword: 5바이트 접근 가능, 3바이트는 포이즌 → 섀도우 값: 5
메모리 접근 시 AddressSanitizer가 삽입하는 코드:
byte *shadow_address = MemToShadow(address);
byte shadow_value = *shadow_address;
if (shadow_value) {
if (SlowPathCheck(shadow_value, address, kAccessSize)) {
ReportError(address, kAccessSize, kIsWrite);
}
}
MemToShadow
: 주소를 섀도우 메모리 영역으로 변환shadow_value
: 접근하려는 메모리 영역의 상태 (0, 1~7, 음수)SlowPathCheck
: 실제 접근이 유효한지 정밀하게 판단
여기서 SLowPathCheck은 다음과 같은 코드로 작성될 수 있다. 이 함수는 접근하려는 마지막 바이트가 포이즌된 영역에 걸쳐 있는지를 검사한다.
bool SlowPathCheck(byte shadow_value, uintptr_t address, size_t kAccessSize) {
size_t last_accessed_byte = (address & 7) + kAccessSize - 1;
return (last_accessed_byte >= shadow_value);
}
추가적으로 Application이 Shadow Memory를 접근하여 ASan을 회피하는 시도를 감지하기 위해서, AddressSanitizer는 섀도우 메모리와 애플리케이션 메모리 사이의 변환 함수를 역방향으로 호출(MemToShadow(ShadowAddr)
)할 경우, 접근이 불가능한 "ShadowGap"이라는 영역에 도달하게 만든다. 이로 인해 프로그램이 섀도우 메모리 자체에 잘못 접근하려고 하면, 즉시 충돌(segfault)이 발생한다.