오늘 끝나면
메모리 취약점
- ✓메모리 취약점의 핵심 문제를 한 문장으로 설명한다
- ✓오른쪽 실습에서 메모리이 어떻게 움직이는지 관찰한다
- ✓다음 강의와 이어지는 한계를 말할 수 있다
실습 미션
버퍼 오버플로우 — 스택을 넘어 쓰면 이 문장이 실제로 무슨 뜻인지 실습에서 한 번 손으로 확인한다.
성공 조건
- □실습의 기본값을 먼저 관찰
- □입력값이나 모드를 한 번 이상 바꿔 결과 비교
- □왜 결과가 바뀌었는지 한 문장으로 설명
화이트해킹 · 10
메모리
취약점
입력이 그릇 크기를 넘으면 옆 메모리로 흘러넘친다.
그 옆에 함수의 반환주소가 있다.
거길 덮으면 실행 흐름이 통째로 넘어간다.
스택, 함수가 쓰는 메모장
함수가 돌 때 자기 변수들을 임시로 쌓아두는 칸이 있다. 그게 스택이다.
입력을 담는 그릇(버퍼)도 여기 잡힌다.
그런데 버퍼 바로 위에 함수가 끝나고 돌아갈 자리인 반환주소가 같이 얹혀 있다.
반환주소는 “이 함수 끝나면 여기로 가”라고 적힌 쪽지다.
함수가 끝나면 CPU는 묻지도 따지지도 않고 그 쪽지대로 점프한다.
버퍼와 반환주소가 한 메모장에 붙어 있다는 게 이 강의 모든 위험의 출발점이다.
크기를 안 재면 흘러넘침
버퍼는 크기가 정해져 있다. 8칸이면 8글자까지다.
그런데 C/C++의 옛 함수들은 입력 길이를 재지 않는다.
strcpy와 gets는 들어온 만큼 그냥 복사한다. 그릇이 넘쳐도 멈추질 않는다.
8칸짜리 그릇에 12글자를 부으면 4글자가 밖으로 샌다.
새는 글자는 사라지는 게 아니라 바로 옆 칸을 덮어쓴다.
옆 칸엔 다른 변수가, 더 위엔 반환주소가 있다. 입력이 길수록 더 깊이 침범한다.
넘치면 반환주소를 덮는다
입력이 길어지면 글자가 버퍼부터 위로 차오른다.
8칸을 넘는 순간 옆 변수를 침범한다.
더 올리면 반환주소 칸까지 빨갛게 덮인다.
반환주소가 덮이면 함수가 끝날 때 공격자가 심어둔 주소로 점프한다.
그럼 공격자가 짠 코드로 실행 흐름이 넘어간다. 이게 제어 흐름 탈취다.
“경계검사 켜짐” 버튼을 누르면 8칸까지만 받고 잘라낸다. 넘쳐도 반환주소가 멀쩡하다.
버퍼 8칸 · 저장변수 2칸 · 반환주소 2칸
입력이 버퍼 안에 들어맞는다. 인접 메모리를 건드리지 않는다.
왜 C/C++에서 흔한가
파이썬과 자바스크립트에선 이게 안 터진다. C/C++에서만 단골이다. 이유가 있다.
C/C++는 속도를 위해 안전장치를 뺐다.
배열에 몇 칸이 있는지 실행 중에 따지지 않는다. 9번째 칸에 써도 언어가 말리지 않는다.
대신 그 책임을 전부 개발자에게 넘긴다.
파이썬은 범위를 넘으면 바로 오류를 내고 멈춘다. C는 조용히 옆 메모리를 망가뜨린다.
이 “조용함” 때문에 버그가 숨고, 그 틈을 공격자가 노린다.
| 언어 | 자동 경계검사 |
|---|---|
| C / C++ | 안 함조용히 옆 칸을 덮는다 |
| Python | 함IndexError로 멈춘다 |
| JavaScript | 함범위 밖이면 undefined |
| Rust | 함컴파일과 실행 중 차단한다 |
방어는 경계를 재고 멈추는 것
방어의 핵심은 한 줄이다. 쓰기 전에 크기를 재고, 넘치면 멈추는 것이다.
길이를 안 재는 함수를 쓰지 않는다.
strcpy 대신 길이를 받는 strncpy나 snprintf를 쓰고, gets 대신 fgets를 쓴다.
입력을 받을 땐 항상 버퍼 크기와 비교하는 경계검사를 넣는다.
컴파일러와 OS도 거든다. 스택에 몰래 심은 표식(카나리)이 깨졌는지 보고, 주소를 매번 섞는다.
가장 확실한 건 경계를 언어가 챙겨주는 Rust나 Go 같은 도구로 옮기는 것이다.
뚫는 법을 배우는 이유는 결국 이 경계를 어디에 둘지 알기 위해서다.
Q. 버퍼 오버플로우가 위험한 이유는? (속도가 느려져서 · 반환주소를 덮어 실행 흐름을 가로채서 · 화면이 깨져서 · 디스크가 차서)
정답은 반환주소를 덮어 실행 흐름을 가로챌 수 있어서다.버퍼를 넘친 입력이 옆에 붙은 반환주소까지 덮으면, 함수가 끝날 때 공격자가 심은 주소로 점프한다.
그럼 공격자 코드로 제어가 넘어간다. 단순한 데이터 손상이 아니라 기기 장악으로 이어진다.
그래서 방어는 항상 경계검사다. 쓰기 전에 크기를 재고 넘치면 멈추는 것이다.
길이를 받아 자른다
최대 크기를 넘긴다
버퍼 크기로 제한한다