원본 항목: Part 1-2. 외부제어·우회 계열 — 9. 시스템 또는 구성 설정의 외부 제어CWE-15repo 폴더: sd09_sysconfig/ (빌드·실행은 §3-1)
목표 시간: 1회차 12분 / 2회차 7분 / 3회차 5분
mock 불필요 — “설정값을 결정하는 함수”만 떼어서 검증한다.
SD02(자원 삽입)와 자매 항목이지만 치환 방식이 다르다는 걸 확인한다.
이 드릴의 문서 구성 (SD01~SD58 공통)
① 요구사항서 — 어떤 코드가 뚫려 있고, 무엇을 만드나.
② pseudo code 템플릿 — Bad 와 Good 의 뼈대이고, 회차마다 가린다.
③ 테스트 코드 — 공격은 막히고 정상은 통과해야 끝이다.
④ 자주 하는 실수
⑤ 모범답안 — 맨 뒤에 접어 둔다.
변형 A/B/C 는 같은 치환 규칙을 다른 소재로 다시 치는 것이다.
0. 이 드릴로 체화할 것
- return atoi(portStr); /* 외부 문자열을 그대로 설정값으로 */+ if (index < 0 || index >= PORT_TABLE_SIZE) return -1;+ return PORT_TABLE[index]; /* 외부에서는 "인덱스"만 받는다 */
SD02가 “범위 검사”였다면 이 항목은 “값 자체를 아예 외부에서 못 받게” 하는 간접 참조(indirection) 다 — 외부는 0, 1, 2 같은 선택지 번호만 알고, 실제 값(포트 21, 2121, 8021)은 내부 테이블에만 존재한다.
회차별 진행
회차
변형
보는 것
순서
1회차
A FTP 서비스 포트 결정
① 요구사항서 ② pseudo code 전체
1. Bad 를 그대로 쳐서 취약점을 관찰한다. 2. Good pseudo 를 한 줄씩 코드로 바꾼다. 3. ③ 테스트를 실행한다.
2회차
B 호스트 ID 설정
① 요구사항서 ② pseudo code 의 번호와 제목만 (Level 2)
1. Bad 를 치고 취약점을 확인한다. 2. 번호만 보고 Good 을 쓴다. 3. ③ 테스트를 실행한다.
3회차
C 테이블 크기를 런타임에 바꿔야 하는 상황
① 요구사항서만
1. ③ 테스트를 먼저 읽는다. 2. 백지에서 Bad 부터 Good 까지 재현한다.
1회차에 막히면 같은 변형을 한 번 더 친다.
3회차를 통과하면 A 를 백지로 다시 친다.
1. 요구사항서
1-A. 변형 A — FTP 서비스 포트 결정
서비스가 사용할 FTP 포트를 결정한다.
허용되는 포트는 미리 정의한 3개(기본 21, 대체1 2121, 대체2 8021)뿐이다.
외부에서는 “몇 번째 대체 포트를 쓸지” 인덱스만 전달받아야 한다.
함수
int resolve_service_port_bad(const char *portStr);int resolve_service_port_good(int index);
신뢰 경계
값
출처
검증 없이 흘러가는 곳
portStr
외부(명령줄 인자/설정 요청)
서비스 포트 설정값
공격 입력표
입력
Bad 결과
이유
"21" (정상)
21
정상 동작
"22"
22
SSH 등 이미 쓰이는 포트와 충돌시켜 서비스 거부(DoS)를 유발할 수 있다
"0"
0
예약 포트(0)를 그대로 받아 시스템이 예기치 않게 동작할 수 있다
1-B. 변형 B — 호스트 ID 설정
PDF 원문의 sethostid() 사례처럼, 문자열이 아니라 정수 하나를 통째로 시스템 설정 함수에 넘기는 대신 미리 정의한 상수(0xC0A80101 등) 중 하나를 인덱스로 선택하도록 변경
1-C. 변형 C — 테이블 크기를 런타임에 바꿔야 하는 상황
포트 후보가 설정 파일에서 동적으로 로드되는 경우, “로드된 테이블도 여전히 서버 내부 자원이고 외부는 인덱스만 받는다”는 원칙이 유지되는지 확인
2. pseudo code 템플릿 — 보고 쳐서 익힌다
사용법
변형별로 아래 pseudo code 를 소스 파일에 주석만 복사한다.
코드는 주석을 보고 내가 쓴다.
다 쓰면 [ ] 를 x 로 바꾼다.
막히면 §5 모범답안(A)의 해당 단계만 본다.
2-A. resolve_service_port_good — Level 1 (1회차, 전부 본다)
먼저 Bad 를 그대로 쳐서 §3 의 test_bad_is_vulnerable 이 통과하는지(= 실제로 뚫리는지) 확인한다.
그다음 Good 을 쓴다.
int resolve_service_port_bad(const char *portStr){ /* 1. return atoi(portStr) ← 외부 문자열이 그대로 설정값 */}int resolve_service_port_good(int index){ /* [ ] 1. 내부 테이블 * PORT_TABLE[] = { 21, 2121, 8021 } * PORT_TABLE_SIZE = sizeof 로 계산 */ /* [ ] 2. 인덱스 범위 검사 * index < 0 또는 (size_t)index >= PORT_TABLE_SIZE → return -1 */ /* [ ] 3. return PORT_TABLE[index] */}
int resolve_service_port_good(int index){ /* 아무것도 안 본다. * §1 요구사항서와 §0 의 치환 규칙만 보고 친다. * * 막히면 아래만 찾아본다. * - `size_t` 와 `int` 비교 시 캐스팅 * - 하한(`< 0`) 검사 */}
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다.
SYNOPSIS 맨 위의 #include 를 확인한다.
로직이 막히면 §3 테스트의 기대값을 먼저 읽는다.
공격 입력표(§1)와 테스트가 답이다.
눈여겨볼 점: resolve_service_port_bad 와 resolve_service_port_good 은 매개변수 타입 자체가 다르다(const char * vs int).
SD08과 마찬가지로, 이 항목의 방어는 “검증 로직 추가”가 아니라 “함수가 받아들이는 정보의 종류를 바꾸는 것”이다.