메모리 오염 영향 평가
취약점의 심각도를 가장 정확하게 파악할 수 있는 방법은 POC 공격을 작성하는 것이지만, 이는 너무 많은 시간을 필요로 한다. 따라서 몇 가지 질문에 대한 답을 찾는 것으로 영향 평가를 수행할 수 있다.
메모리에서 버퍼의 위치
변수는 주로 스택, 힙, 지속 데이터(정적 변수, 전역 변수 포함) 이렇게 세 개의 메모리 영역에 저장되지만 종종 이 세 위치를 분할하거나 새로운 영역을 나누기도 한다. 때로는 초기화된 전역 변수와 초기화 되지 않은 전역 변수를 나누기도 하고, 특별할 위치에 TLS를 두기도 한다. 또한 공유 라이브러리는 라이브러리 코드 바로 다음에 위치하는 프로세스 메모리에 초기화되거나 비초기화된 상태에서 매핑된다. 어디서 메모리 오염이 일어나고 어떤 특별한 고려 사항이 적용되는지 파악할 필요성이 있으며, 운영체제 별 고유 메모리 배치에 대한 이해가 필요하다.
다른 데이터로 덮어쓰기 되는 것
메모리 오염은 공격자가 목표하는 변수에만 국한되지 않을 수 있으며, 다른 변수에 까지 값을 덮어 쓸 수 있다. 예제에서 보는 바와 같이 프로그램 카운터를 덮어쓰기전 지역 변수 값을 같이 덮어쓸 수 잇다.
지역 변수로의 오버플로우
int dostuff(char *login)
{
char *ptr = (char *)malloc(1024);
char buf[1024];
...
strcpy(buf, login);
...
free(ptr);
return 0;
}
여기서 공격자는 프로그램 카운터를 덮어쓸 때 ptr
변수도 같이 덮어쓰게 되는데, ptr
변수는 프로그램이 리턴되기 바로 전에 해제되는 값이다. 이로 인해 유효하지 않은 주소 값으로 ptr
변수를 덮어 쓰게 되면 free()
함수 호출 시 프로그램 크래시가 발생할 수 있다. 이로 인해 단순히 프로그램 카운터를 덮어쓰는 것보다 훨씬 복잡해졌다. 이 처럼 버퍼 오버플로우 취약점 위험 평가 시에는 공격 시도를 완화할 수 있는 경로의 모든 변수에 관심을 기울여야 한다.
덮어쓰기가 가능한 바이트 수
정해진 크기로만 오버플로우를 일으키는 경우 공격을 좀더 어렵게 한다. 하지만 여전히 취약점 공격이 가능하다. 적은 수의 바이트만 변조된다면 취약점 공격 가능성은 어떤 데이터가 오염되는지에 따라 달려 있다. 메모리에서 다시 사용되지 않는 변수 값만 변경할 수 있다면 이 버그는 취약점을 공격하는 데 도움이 되지 않는다.
반대로 많은 양을 변조 시킬 수 있다면 이 버그는 메모리의 많은 부분을 오염시키고 프로세스를 파괴할 가능성이 높다. 가장 일반적인 예는 예외가 발생한 후 참조하는 함수 포인터 값을 갖고 있는 SEH 구조체를 닾어쓰는 윈도우상의 스택 기반 오버플로우다.
임의 장소에 다발적인 쓰기를 발생시키는 경우도 존재한다. 이와 같은 경우 공격자는 어떻게 공격할지에 대한 많은 선택권을 가지게 된다.
1, 2바이트의 덮어쓰기가 4바이트 덮어쓰기보다 공격이 더 쉬울 때가 있다. 예를 들어 포인터를 덮어쓴다고 할 때, 포인터가 객체를 가리키게 하는 대신 데이터 버퍼를 가리키게 포인터 값 중 최하위 바이트를 덮어씀으로써 안정적으로 취약점을 공격할 수 있다.
메모리를 오염시키는 데 사용될 수 있는 데이터
일부 취약점은 메모리를 덮어쓰는 데 사용된 데이터를 직접 제어할 수 없다. 이 데이터가 문자 제약이 있는지, 한 바이트 덮어쓰기인지, 공격자가 다루기 용이한 memset()
호출과 함깨 쓰이지는지에 따라 쓰임이 제한적일 수 있다.
간접적 메모리 오염
int process_string(char *string)
{
char **tokens, *ptr;
int tokencount;
token = (char **)calloc(64, sizeof(char *));
if(!tokens)
return -1;
for(ptr = string; *ptr;)
{
int c;
for(end = ptr; *end && !isspace(end); end++);
c = *end;
*end = '\0';
tokens[tokencount++] = ptr;
ptr = (c == 0 ? end : end + 1);
}
...
메모리를 덮어쓰는 데 사용한 데이터는 공격자에 의해 직접 제어되지 않지만, 덮어쓰기 된 메모리는 공격자가 제어 가능한 데이터를 가리키는 포인터를 포함한다.
Off-by-One 덮어쓰기
Off-by-one 취약점은 공격자가 제어하지 않는 데이터에 덮어쓰기와 관련된 가장 일반적인 취약점 중 하나이다.
struct {
int sequence;
int mac[MAX_MAC];
char *key;
};
int delete_session(struct session * session)
{
memset(session->key, 0, KEY_SIZE);
free(session->key);
free(session);
}
int get_mac(int fd, struct session *session)
{
unsigned int i, n;
n = read_network_integer(fd);
if(n > MAX_MAC)
return -1;
for(i = 0; i <= n; i++)
session->mac[i] = read_network_integer(fd);
...
공격자가 mac
의 길이를 정확하게 MAX_MAC
으로 지정했다면 get_mac()
함수는 할당된 공간보다 한 엘리먼트를 더 읽게 된다. 이 경우 마지막에 읽어 들인 정수 값은 key
변수에 덮어 쓰이게 된다. delete_session()
함수가 호출되면 key
변수가 비워지기 전에 memset
으로 값이 넘겨지는데, 이것으로 공격자는 메모리의 임의의 장소에 NULL 바이트 뿐만 아니라 그 밖의 값을 덮어쓸 수 있다. 이런 종류의 취약ㅈ머은 어떤 데이터를 덮어쓰기 할지 선택할 수 없으므로 복잡하다.
메모리 블록의 공유
메모리 관리자가 오동작으로 한 번 이상 같은 메모리 블록을 배포할 때 애플리케이션에서 발생한다. 이런 취약점은 다음 두가지 이유 중 하나로 발생한다.
- 메모리 관리 코드의 버그
- 올바르게 사용되지 않는 메모리 관리 API
정리
메모리 오염 취약점 공격 분야는 계속적으로 연구 중이고, 불가능했던 것으로 간주되던 부분을 이용해 공격하는 새로운 방법을 찾아내면서 계속적으로 진화하고 있다. 그러므로 검토자로서는 안전하다고 증명될 때까지는 모든 메모리 오염 이슈를 잠재적으로 심각한 취약점으로 취급할 필요가 있다.