테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

시스템·소프트웨어 보안

프로그램이 메모리를 잘못 다룰 때 생기는 문제와, 운영체제와 배포 과정의 방어다.

목차 (6)
  1. 메모리 안전: 버퍼 끝을 넘어 쓰면 무슨 일이 생기나
  2. 완화 기법: 스택 카나리, ASLR, DEP/NX
  3. 메모리 안전 언어: 검사를 사람 대신 언어에 맡긴다
  4. 샌드박스와 권한 분리: 뚫린 뒤에도 피해를 가둔다
  5. 공급망 보안: 내가 쓰지 않은 코드도 내 코드다
  6. 코드 서명과 안전한 업데이트

메모리 안전: 버퍼 끝을 넘어 쓰면 무슨 일이 생기나

프로그램이 쓰는 메모리는 번호(주소)가 붙은 칸이 길게 이어진 띠다. C와 C++ 같은 언어는 "이 주소부터 10바이트를 써라"는 명령을 그대로 실행하고, 그 10바이트가 정말 내 것인지는 따지지 않는다. 이 검사를 개발자가 빠뜨리면 생기는 오류를 묶어 메모리 안전(memory safety) 문제라고 한다. 대표가 버퍼 넘침(buffer overflow), 정해진 크기의 칸 묶음(버퍼) 끝을 넘어 쓰는 실수다.

함수가 불리면 그 함수의 지역 변수와 "끝나면 돌아갈 주소"가 스택(stack)이라는 영역에 나란히 놓인다. 8칸짜리 버퍼에 길이를 확인하지 않고 14글자를 복사하면, 넘친 글자는 사라지지 않고 바로 옆 칸, 곧 다른 변수와 돌아갈 주소에 들어간다. 변수가 바뀌면 프로그램은 바뀐 값을 그대로 믿고, 돌아갈 주소가 바뀌면 함수가 끝날 때 엉뚱한 곳으로 간다. 대개는 그냥 죽지만, 입력을 고르는 사람이 공격자라면 실행 흐름을 원하는 쪽으로 돌릴 여지가 생긴다. 이것이 수십 년 동안 가장 흔한 원격 공격 통로였던 이유다.

스택 프레임 단계 그림: 8칸 버퍼 오른쪽에 옆 변수와 돌아갈 주소가 붙어 있고, 긴 입력을 검사 없이 복사하면 넘친 글자가 옆 변수와 돌아갈 주소를 차례로 덮는다. 마지막 단계는 길이 검사로 복사를 거부한다buf[8]옆 변수돌아갈 주소함수가 불린 직후: 버퍼는 비어 있다0호출한 곳입력 "HELLO"(5글자) — 버퍼 안에 들어간다HELLO0호출한 곳입력 "HELLOWORLD"(10글자) — 2글자가 넘친다HELLOWORLD호출한 곳입력 14글자 — 돌아갈 주소까지 덮인다HELLOWORLD1234경계 검사: 10 > 8이므로 복사하지 않고 오류를 돌려준다0호출한 곳낮은 주소높은 주소 (쓰기가 나아가는 방향)버퍼 안에 쓴 글자버퍼 밖으로 덮어쓴 칸아직 안 쓴 칸
1 / 5

함수가 불리면 스택 프레임에 버퍼 8칸, 옆 변수, 돌아갈 주소가 나란히 놓인다.

버퍼 넘침(buffer overflow)의 원리. 메모리는 칸이 이어진 띠라서, 버퍼 끝을 넘어 쓴 글자는 그냥 옆 칸에 들어간다. 옆 칸이 무엇이든 가리지 않는다.

버그 자체는 단순하다. 아래 두 조각의 차이는 복사하기 전에 길이를 보느냐 하나다.

/* 위험: input이 몇 글자든 buf에 그대로 복사한다 */
char buf[8];
strcpy(buf, input);

/* 고친 것: 버퍼보다 길면 복사하지 않고 오류를 돌려준다 */
char buf[8];
size_t length = strlen(input);
if (length >= sizeof buf) return ERROR_TOO_LONG;
memcpy(buf, input, length + 1);

메모리 안전 문제는 경계 밖 쓰기만이 아니다. 자주 나오는 종류는 다음과 같다.

종류무엇이 잘못되나
경계 밖 쓰기·읽기(out-of-bounds write/read)버퍼 끝을 넘어 옆 데이터를 덮거나, 남의 데이터(비밀번호, 키)를 읽어 밖으로 내보낸다
해제 후 사용(use-after-free)돌려준 메모리를 계속 쓴다. 그 자리에 이미 다른 데이터가 들어와 있어 엉뚱한 값을 믿게 된다
이중 해제(double free)같은 메모리를 두 번 돌려줘 메모리 관리 장부가 꼬인다
초기화 안 된 메모리값을 넣기 전에 읽어, 예전에 그 자리에 있던 데이터가 새어 나간다
정수 넘침(integer overflow)크기 계산이 넘쳐 작은 수가 되고, 너무 작은 버퍼를 잡은 뒤 위의 경계 밖 쓰기로 이어진다

MITRE의 약점 분류에서 CWE-787 경계 밖 쓰기는 해마다 가장 위험한 약점 25가지(CWE Top 25)의 상위권에 빠지지 않는다. Microsoft 보안 대응 센터는 2019년 글에서 해마다 자사 제품에 매기는 CVE의 약 70%가 메모리 안전 문제라고 밝혔고, Chromium 프로젝트도 심각한 보안 버그의 약 70%가 같은 종류라고 적었다. 두 거대한 코드베이스가 서로 다른 조사에서 같은 비율에 닿았다는 점이 이 문제가 개인의 부주의가 아니라 언어의 성질이라는 근거로 자주 쓰인다.

완화 기법: 스택 카나리, ASLR, DEP/NX

수십억 줄의 C·C++ 코드를 하루아침에 고칠 수는 없으므로, 운영체제와 컴파일러는 버그가 남아 있어도 악용하기 어렵게 만드는 장치를 겹겹이 더해 왔다. 이것을 완화 기법(mitigation)이라 한다. 버그를 없애는 것이 아니라 버그를 공격으로 바꾸는 비용을 올리는 것이므로, 보안의 기초에서 말한 심층 방어의 한 겹으로 보는 것이 맞다.

기법하는 일한계
스택 카나리(stack canary)컴파일러가 버퍼와 돌아갈 주소 사이에 실행마다 바뀌는 감시값을 넣고, 함수가 돌아가기 직전에 값이 그대로인지 확인한다. 바뀌었으면 넘침으로 보고 프로그램을 멈춘다돌아갈 주소를 지키는 장치라, 카나리 앞의 변수만 바뀌는 넘침이나 힙의 버그는 못 잡는다
ASLR(주소 공간 배치 무작위화)프로그램 코드, 라이브러리, 스택, 힙이 놓이는 주소를 실행할 때마다 무작위로 바꾼다. 공격에 필요한 "어디로 뛸지"를 미리 알 수 없게 한다주소 하나가 새어 나가는 다른 버그(정보 누출)와 엮이면 효과가 크게 줄어든다
DEP / NX(실행 금지 비트)CPU와 운영체제가 메모리 쪽마다 권한을 단다. 데이터를 쓰는 스택·힙은 실행할 수 없고, 코드 영역은 고쳐 쓸 수 없다(W^X: 쓰기와 실행 중 하나만)이미 있는 코드 조각을 이어 붙이는 방식은 막지 못해, 제어 흐름 무결성(CFI) 같은 다음 겹이 더해졌다
완화 기법 세 칸: 스택 카나리는 버퍼와 돌아갈 주소 사이에 감시값을 두어 넘침을 알아채고, ASLR은 실행마다 메모리 배치를 바꾸며, DEP/NX는 쓸 수 있는 곳을 실행하지 못하게 나눈다스택 카나리ASLRDEP / NX돌아갈 주소카나리buf넘친 쓰기는 카나리부터 덮는다돌아가기 전 값이 바뀌었으면프로그램을 멈춘다스택힙코드실행 1스택힙코드실행 2실행마다 위치가 바뀌어주소를 미리 알 수 없다코드 영역읽기 · 실행스택 · 힙읽기 · 쓰기스택에 쓴 데이터 실행 → 거부쓸 수 있는 곳은 실행할 수없다 (W^X 원칙)셋 다 버그를 없애지 않는다. 버그를 악용하기 어렵게 만들 뿐이다보호 장치가 놓이는 곳넘친 쓰기 · 막히는 동작
세 가지 완화 기법. 카나리는 넘침을 알아채고, ASLR은 목표 주소를 숨기고, DEP/NX는 데이터가 코드로 실행되는 길을 막는다. 셋 모두 기본으로 켜 두되, 버그를 고친 것으로 여기지 않는다.

현대 운영체제와 컴파일러에서는 이 장치들이 대부분 기본으로 켜져 있다. 직접 빌드하는 C·C++ 프로젝트라면 컴파일러의 보호 옵션과 운영체제의 실행 파일 보호 기능이 꺼져 있지 않은지 확인하는 정도가 개발자의 몫이다. 완화 기법이 해마다 늘어도 메모리 안전 버그 비율이 70% 근처에 머물렀다는 사실이, 다음 절의 근본 처방으로 관심을 옮긴 계기다.

메모리 안전 언어: 검사를 사람 대신 언어에 맡긴다

메모리 안전 언어(memory-safe language)는 경계 검사와 메모리 수명 관리를 언어 차원에서 강제한다. Java, C#, Go, Python, JavaScript는 가비지 컬렉터가 메모리를 대신 돌려주고 배열에 접근할 때마다 범위를 확인한다. Rust는 가비지 컬렉터 없이, 컴파일할 때 소유권(ownership)과 빌림(borrowing) 규칙으로 해제 후 사용 같은 오류를 아예 컴파일되지 않게 막는다. Swift도 기본적으로 메모리 안전하게 설계됐다.

같은 범위 밖 쓰기를 두 줄로 비교: C와 C++은 검사 없이 옆 메모리를 덮고 계속 실행하지만, 메모리 안전 언어는 범위 검사에서 거부하고 그 자리에서 멈춘다C · C++검사는 개발자 몫arr[10] = 값배열 크기는 8검사 없음주소만 계산해 쓴다옆 메모리 덮음조용히 계속 실행Rust · Go · Java언어가 대신 검사arr[10] = 값배열 크기는 8범위 검사10 ≥ 8 이면 거부그 자리에서 멈춤오류·패닉·예외버그는 둘 다 있다. 공격 통로로 남느냐, 고칠 오류로 드러나느냐가 다르다메모리가 망가짐안전하게 실패함
같은 실수, 다른 결과. 크기 8인 배열의 10번 칸에 쓰는 버그는 어느 언어에서나 생길 수 있다. 메모리 안전 언어에서는 그 버그가 메모리를 망가뜨리기 전에 오류로 멈춘다.

미국 사이버보안·기반시설보안청(CISA)과 국가안보국(NSA)은 여러 나라 기관과 함께 낸 The Case for Memory Safe Roadmaps(2023)에서 소프트웨어 제조사가 메모리 안전 언어로 옮겨 갈 계획(로드맵)을 세워 공개하라고 권했다. 2025년의 Memory Safe Languages: Reducing Vulnerabilities in Modern Software Development는 그 근거와 현실적인 걸림돌을 정리한다. 요지는 다음과 같다.

  • 모든 코드를 다시 쓸 필요는 없다. 새로 쓰는 코드와 외부 입력을 직접 다루는 위험한 부분(파서, 네트워크 처리, 미디어 디코더)부터 메모리 안전 언어로 옮기는 것이 효과가 크다.
  • 메모리 안전 언어에도 검사를 끄는 탈출구(Rust의 unsafe, 다른 언어의 C 연동)가 있다. 그 부분은 작게 모으고 따로 검토한다.
  • 옮기는 동안 남은 C·C++ 코드에는 완화 기법, 퍼징(fuzzing, 무작위 입력을 대량으로 넣어 보는 시험), 정적 분석을 함께 쓴다.

샌드박스와 권한 분리: 뚫린 뒤에도 피해를 가둔다

아무리 조심해도 버그는 남는다. 그래서 "뚫리지 않게"와 함께 "뚫려도 멀리 못 가게"를 설계한다. 권한 분리(privilege separation)는 프로그램을 여러 조각으로 나누고, 위험한 입력을 다루는 조각일수록 권한을 적게 주는 방식이다. 최소 권한 원칙을 프로그램 구조에 적용한 것이다. 조각을 가두는 울타리가 샌드박스(sandbox)로, 운영체제가 그 프로세스의 파일·네트워크·장치 접근을 막는다.

웹 브라우저가 가장 잘 알려진 예다. 브라우저는 인터넷의 아무 페이지나 열어 그 안의 HTML, 이미지, 자바스크립트를 해석해야 하므로 공격 표면이 넓다. Chromium의 샌드박스 설계에서 페이지를 해석하는 렌더러(renderer) 프로세스는 파일을 열거나 네트워크에 직접 연결할 수 없다. 필요한 일은 프로세스 사이 메시지(IPC)로 권한이 큰 브라우저 프로세스에 부탁하고, 브라우저 프로세스는 요청이 정당한지 검사한 뒤 대신 처리한다. 여기에 사이트 격리(site isolation)가 더해져 사이트마다 렌더러를 따로 띄우므로, 한 사이트의 렌더러가 뚫려도 다른 사이트의 로그인 정보가 같은 프로세스 메모리에 있지 않다.

브라우저 프로세스 구조: 위에 파일과 네트워크에 접근할 수 있는 브라우저 프로세스가 있고, 아래 샌드박스 안에 사이트마다 따로 뜬 렌더러 세 개가 있다. 렌더러는 IPC로 요청만 보낸다브라우저 프로세스파일 · 네트워크 · 화면 (권한 큼)GPU 프로세스네트워크 서비스렌더러a.com 탭렌더러b.com 탭렌더러c.com (iframe)IPC 요청검사 후 처리샌드박스: 파일·장치·다른 사이트에 직접 손댈 수 없다샌드박스 경계IPC(프로세스 사이 메시지)권한이 큰 프로세스
브라우저의 권한 분리. 웹 페이지를 해석하는 렌더러는 사이트마다 따로 뜨고 샌드박스에 갇혀 있다. 렌더러에서 버그가 터져도, 공격자는 샌드박스와 브라우저 프로세스의 검사를 또 넘어야 한다.

같은 생각은 곳곳에 있다. 스마트폰 앱은 저마다 샌드박스에서 돌며 카메라·위치 같은 권한을 사용자에게 따로 받는다. 서버에서는 컨테이너와 가상 머신이 서비스를 서로 떼어 놓고, 웹 서버는 관리자 권한으로 포트만 연 뒤 일반 사용자로 권한을 낮춰 요청을 처리한다. 이 사이트의 도구들처럼 웹 페이지로 도는 프로그램은 모두 이 샌드박스 안에서 실행된다.

사이트 격리가 기본값이 된 계기는 소프트웨어 버그가 아니라 CPU였다. 2018년에 알려진 Spectre와 Meltdown은 CPU가 성능을 위해 미리 해 보는 투기적 실행의 흔적이 캐시에 남는 점을 이용해, 같은 프로세스나 커널의 메모리를 엿볼 수 있음을 보였다. 이런 공격을 실행 시간이나 캐시 상태 같은 부수 정보로 비밀을 알아내는 부채널 공격(side channel)이라 한다. 코드가 완벽해도 같은 하드웨어를 나눠 쓰면 새어 나갈 수 있으므로, Chrome은 2018년 67판에서 모든 사이트에 사이트 격리를 기본으로 켜 비밀을 아예 다른 프로세스에 두었다. 투기적 실행 자체는 컴퓨터 구조 노트에 있다.

공급망 보안: 내가 쓰지 않은 코드도 내 코드다

요즘 앱의 코드는 대부분 남이 쓴 것이다. 패키지 하나를 설치하면 그 패키지가 쓰는 패키지, 또 그 패키지가 쓰는 패키지가 줄줄이 따라 들어온다. 이 간접 의존성(transitive dependency)까지 합친 전체가 앱과 같은 권한으로 실행된다. 개발 도구, 빌드 서버, 배포 경로까지 포함해 소프트웨어가 사용자에게 닿기까지의 모든 고리를 소프트웨어 공급망(software supply chain)이라 부른다. OWASP Top 10도 2025년판에서 소프트웨어 공급망 실패를 별도 항목으로 올렸다.

의존성 나무: 내 앱이 직접 쓰는 패키지 A, B, C 아래에 간접 의존성 D, E, F, G가 있고, F가 쓰는 H가 오염되면 F와 B를 거쳐 내 앱까지 들어온다내 앱ABCDEFGH (오염)직접간접더 깊이직접 고른 것은 셋이지만 앱에 들어온 패키지는 여덟이다오염된 패키지오염이 앱까지 올라오는 길
의존성 나무. 직접 고른 패키지는 셋이지만 그 아래로 다섯이 더 들어온다. 깊은 곳의 H가 오염되면 F와 B를 거쳐 앱 전체가 영향을 받는다. 공급망 공격은 지키기 쉬운 앱 대신 지키기 어려운 이 고리를 노린다.

실제로 있었던 일은 대체로 세 갈래다. 관리자가 바뀐 인기 패키지에 악성 의존성이 끼어든 일(2018년 npm event-stream), 빌드 시스템이 침해돼 정식 서명된 업데이트에 백도어가 실린 일(2020년 SolarWinds Orion), 몇 년에 걸쳐 신뢰를 쌓은 기여자가 압축 라이브러리에 백도어를 숨긴 일(2024년 xz utils)이다. 셋 모두 코드 자체의 버그가 아니라 "누구를, 무엇을 믿었나"의 문제였다.

방어하는 일
lockfile과 고정 설치package-lock.json 같은 lockfile은 나무 전체의 정확한 버전과 내용 해시를 적어 둔다. CI에서는 npm ci처럼 lockfile과 어긋나면 멈추는 명령으로 설치해, 어제와 오늘 다른 코드가 몰래 들어오지 않게 한다
패키지 이름 확인인기 패키지와 한 글자 다른 이름으로 올린 가짜(타이포스쿼팅), 사내 패키지와 같은 이름을 공개 저장소에 더 높은 버전으로 올려 대신 설치되게 하는 수법(의존성 혼동)이 있다. 설치 전에 이름과 게시자를 확인하고, 사내 패키지는 범위(scope)를 정해 사내 저장소에서만 받게 설정한다
의존성 줄이기와 업데이트 검토몇 줄짜리 기능에 패키지를 들이지 않는다. 업데이트는 자동으로 받되 바뀐 내용을 보고 합치며, 알려진 취약점이 있는 버전은 경고 도구로 찾아 올린다
SBOM(소프트웨어 자재 명세서)제품에 들어간 구성 요소와 버전의 목록이다. 새 취약점이 발표됐을 때 "우리 제품에 그게 들어 있나"에 몇 분 만에 답하게 해 준다(CISA SBOM)
SLSA와 출처 증명SLSA(Supply-chain Levels for Software Artifacts)는 빌드 과정의 신뢰 수준을 단계로 나눈 틀이다. 결과물이 어느 소스에서 어떤 빌드로 나왔는지 위조하기 어려운 출처 증명(provenance)을 남기는 것이 핵심이다
서명된 릴리스배포물에 서명해 받는 쪽이 출처를 확인하게 한다. Sigstore는 긴 수명의 키를 관리하지 않고도 서명하고 공개 기록에 남기게 해 준다

조직 전체의 개발 과정에 이런 습관을 심는 기준으로는 NIST의 보안 소프트웨어 개발 프레임워크(SSDF, SP 800-218)가 있고, 실무 점검표로는 OWASP 공급망 보안 치트 시트가 쓸 만하다.

코드 서명과 안전한 업데이트

업데이트는 보안의 생명줄이면서 가장 강력한 공격 통로다. 사용자가 믿고 자동으로 설치하기 때문이다. 그래서 업데이트는 반드시 코드 서명(code signing)으로 지킨다. 개발자는 결과물의 해시에 자기 개인키로 디지털 서명을 하고, 기기는 미리 받아 둔 공개키로 서명을 검증해 맞을 때만 설치한다. 파일이 한 바이트만 바뀌어도 해시가 달라져 검증이 실패한다. 해시가 어떻게 달라지는지는 해시 계산 도구로 직접 볼 수 있다.

서명된 업데이트의 흐름: 개발자 쪽은 빌드 결과물의 해시를 개인키로 서명해 배포 서버에 올리고, 기기 쪽은 내려받은 파일을 공개키로 검증해 맞으면 설치하고 틀리면 거부한다개발자 쪽 (개인키를 가짐)빌드 결과물해시 계산개인키로 서명배포 서버파일 + 서명내려받기공개키로 검증통과 → 설치불일치 → 거부기기 쪽 (공개키를 미리 가짐)서버가 털려도 개인키 없이는 서명을 못 만든다키를 쓰는 단계
서명된 업데이트. 배포 서버나 중간 경로가 털려도, 개인키 없이는 기기가 받아들이는 서명을 만들 수 없다. 그래서 개인키는 빌드 서버와 떼어 하드웨어 보안 모듈 같은 곳에 따로 보관한다.

HTTPS로 내려받는 것만으로는 부족하다. HTTPS는 "이 서버와 안전하게 통신했다"만 보장할 뿐, 서버에 올라간 파일이 진짜인지는 말해 주지 않는다. 서명은 서버를 믿지 않아도 되게 한다. 서명만으로도 막지 못하는 공격이 있는데, 예전에 정식으로 서명된 취약한 버전을 다시 내미는 되돌리기(rollback) 공격과, 새 업데이트가 없다고 속여 기기를 옛 버전에 묶어 두는 동결(freeze) 공격이 그렇다. The Update Framework(TUF)는 버전 번호와 만료 시각까지 서명하고, 역할마다 키를 나눠 키 하나가 새어도 전체가 무너지지 않게 설계한 업데이트 표준이다.

  • 서명 키는 빌드 서버와 분리하고, 가능하면 하드웨어에 넣어 꺼낼 수 없게 한다.
  • 버전은 앞으로만 간다. 기기는 이미 설치한 것보다 낮은 버전을 거부한다.
  • 업데이트 확인이 오래 실패하면 사용자에게 알린다. 조용히 옛 버전에 머무는 것이 가장 위험하다.
  • 키가 새는 날을 대비해, 키를 바꾸고 옛 키를 폐기하는 절차를 미리 만들어 둔다.

같은 생각은 전원을 켜는 순간부터 적용된다. 보안 부팅(Secure Boot)은 펌웨어가 서명을 확인한 부트로더만 실행하고, 부트로더가 다시 운영체제 커널의 서명을 확인하는 신뢰의 사슬이다(운영체제 노트의 부팅 절). 여기에 메인보드의 보안 칩인 TPM(Trusted Platform Module)이 더해진다. TPM은 키를 칩 안에서 만들고 보관하며, 부팅 단계마다 무엇이 실행됐는지 측정값을 남긴다. 측정값이 예상과 다르면 디스크 암호화 키를 내주지 않게 정책을 걸 수 있고, 원격 증명(attestation)으로 다른 시스템에 이 기기가 변조되지 않았음을 보일 수 있다. 제로 트러스트의 기기 상태 확인이 기대는 바탕이다.