서론: 열려진 성문, C언어의 양날의 검
1988년, '모리스 웜(Morris Worm)'이라는 악성 코드가 전 세계 인터넷의 10%를 마비시킨 사건이 있었습니다. 이 역사적인 사건의 원인은 놀랍게도 복잡한 암호 해독이 아니었습니다. 단지 코딩할 때 입력 값의 크기를 검사하지 않은 작은 실수, 바로 '버퍼 오버플로우(Buffer Overflow)' 때문이었습니다.
제가 NASA에서 비행 소프트웨어를 검수할 때 가장 눈에 불을 켜고 찾는 것이 바로 이 오류입니다. C언어는 개발자에게 메모리를 직접 제어할 권한(포인터)을 주지만, 그에 따른 안전장치는 제공하지 않습니다. 마치 브레이크 없는 스포츠카와 같죠.
오늘 11편에서는 컵에 물이 넘치는 단순한 현상이 어떻게 시스템의 제어권을 탈취하는 보안 취약점으로 변질되는지, 그 위험한 메커니즘을 파헤쳐 봅니다.
본론 1: 컵(Buffer)에 물이 넘치면 어디로 가는가?
'버퍼(Buffer)'는 데이터를 잠시 저장하는 메모리 공간(배열 등)을 말합니다. 100ml 용량의 컵(버퍼)이 있다고 가정해 봅시다. 여기에 120ml의 물을 부으면 어떻게 될까요?
- 현실 세계: 물이 바닥으로 넘쳐흐릅니다. (단순 낭비)
- 컴퓨터 세계: 넘친 20ml의 데이터는 사라지지 않고, 바로 옆집 메모리 공간을 침범합니다. (데이터 파괴)
이것이 C언어의 가장 무서운 점입니다. char buf[8];이라고 선언해 놓고 10글자를 억지로 밀어 넣으면, C언어는 경고 없이 인접한 메모리(중요한 변수나 시스템 데이터)를 덮어써 버립니다(Overwrite).
본론 2: 왜 이것이 해킹의 통로가 되는가?
단순히 옆집 데이터를 지우는 정도라면 시스템이 멈추고 말겠지만(Crash), 악의적인 공격자는 이를 교묘하게 이용합니다.
지난 4편에서 배운 스택(Stack) 구조를 떠올려 보십시오. 함수 내의 변수(버퍼) 바로 옆에는 '복귀 주소(Return Address)'라는 매우 중요한 데이터가 살고 있습니다. 이 주소는 함수가 끝난 뒤 "다시 돌아가야 할 코드의 위치"를 담고 있습니다.
돌아갈 집을 바꿔치기하다
만약 공격자가 버퍼를 넘치게 채워서(Overflow), 이 복귀 주소가 적힌 공간까지 자신의 데이터로 덮어버린다면 어떻게 될까요?
- 함수가 종료됩니다.
- CPU는 복귀 주소를 확인하고 돌아가려 합니다.
- 하지만 주소는 이미 공격자가 심어놓은 악성 코드가 있는 위치로 조작되어 있습니다.
- CPU는 아무것도 모른 채 악성 코드를 실행합니다. 시스템 통제권이 넘어가는 순간입니다.
본론 3: NASA 엔지니어의 방어 전략
그렇다면 우리는 C언어를 쓰지 말아야 할까요? 아닙니다. 이 위험을 알고 방어 코딩(Defensive Coding)을 하는 것이 핵심입니다.
- 경계 검사(Bounds Checking): 데이터를 복사하기 전에, 버퍼의 크기보다 데이터가 큰지 반드시 확인해야 합니다. "사용자의 입력은 절대 신뢰하지 않는다"가 철칙입니다.
- 안전한 함수 사용:
strcpy(),gets()같은 함수는 길이 검사를 하지 않는 시한폭탄입니다. 대신 길이를 지정하는strncpy(),fgets()같은 안전한 표준 함수를 사용해야 합니다. - 카나리아(Canary) 기법: 광산의 카나리아처럼, 버퍼와 복귀 주소 사이에 '특정 값'을 심어둡니다. 함수가 끝날 때 이 값이 변해있다면 오버플로우가 발생한 것으로 간주하고 프로그램을 즉시 강제 종료시켜 공격을 차단합니다.
결론: 자유에는 책임이 따른다
C언어는 개발자에게 하드웨어를 제어할 무한한 자유를 주었습니다. 하지만 버퍼 오버플로우는 그 자유를 남용했을 때 어떤 대가를 치러야 하는지 보여주는 대표적인 사례입니다.
훌륭한 개발자는 '돌아가는 코드'를 짜는 사람이 아니라, '부서지지 않는 코드'를 짜는 사람입니다. 메모리의 경계를 지키는 것, 그것이 보안의 시작입니다.
다음 [Part 12. 메모리 누수(Memory Leak): 닫지 않은 수도꼭지] 편에서는, 오버플로우와는 반대로 메모리를 다 쓰고 돌려주지 않아서 시스템을 서서히 질식시키는 또 다른 치명적 실수에 대해 알아보겠습니다.