원본 항목: Part 7. API 오용 — 1. DNS lookup에 의존한 보안결정CWE-247repo 폴더: sd54_dnstrust/ (빌드·실행은 §3-1)
목표 시간: 1회차 12분 / 2회차 7분 / 3회차 5분
Part 7(API 오용)의 첫 드릴이다.
실제 gethostbyname()은 네트워크·시스템 DNS 설정에 의존해 테스트가 실행 환경마다 달라지므로, “DNS 캐시가 포이즈닝됐다”는 상황 자체를 인자(poisoned)로 직접 제어해 공격이 성공하는 순간을 결정론적으로 재현한다.
이 드릴의 문서 구성 (SD01~SD58 공통)
① 요구사항서 — 어떤 코드가 뚫려 있고, 무엇을 만드나.
② pseudo code 템플릿 — Bad 와 Good 의 뼈대이고, 회차마다 가린다.
③ 테스트 코드 — 공격은 막히고 정상은 통과해야 끝이다.
④ 자주 하는 실수
⑤ 모범답안 — 맨 뒤에 접어 둔다.
변형 A/B/C 는 같은 치환 규칙을 다른 소재로 다시 치는 것이다.
0. 이 드릴로 체화할 것
- trustedSites.insert("www.trust.com"); /* 도메인명 기준 */- hostent *record = gethostbyname(); /* DNS 조회 의존 */- if (trustedSites.count(targetSite) > 0) SendContract(ip_address);+ trustedSiteIPs.insert("232.234.89.52"); /* IP 기준 */+ if (trustedSiteIPs.count(targetSiteIp) > 0) SendContract(targetSiteIp); /* DNS 개입 없음 */
“이름은 속일 수 있다.” 보안 판정의 기준 값은 DNS가 개입하지 않는 값(IP 화이트리스트)으로 잡아야 한다.
회차별 진행
회차
변형
보는 것
순서
1회차
A 계약서 전송 대상 결정
① 요구사항서 ② pseudo code 전체
1. Bad 를 그대로 쳐서 취약점을 관찰한다. 2. Good pseudo 를 한 줄씩 코드로 바꾼다. 3. ③ 테스트를 실행한다.
2회차
B 로그인 리다이렉트 대상 검증으로 변형
① 요구사항서 ② pseudo code 의 번호와 제목만 (Level 2)
1. Bad 를 치고 취약점을 확인한다. 2. 번호만 보고 Good 을 쓴다. 3. ③ 테스트를 실행한다.
3회차
C IP도 위조 가능하다는 한계까지 확장
① 요구사항서만
1. ③ 테스트를 먼저 읽는다. 2. 백지에서 Bad 부터 Good 까지 재현한다.
1회차에 막히면 같은 변형을 한 번 더 친다.
3회차를 통과하면 A 를 백지로 다시 친다.
int decide_send_target_good(const char *claimed_ip, char *out_ip, size_t out_size){ /* [ ] 1. 시그니처에서 hostname, poisoned 를 **없앤다** */ /* [ ] 2. IP 화이트리스트를 직접 비교 */ /* [ ] 3. snprintf(out_ip, out_size, "%s", claimed_ip) */ /* [ ] 4. return 0 */}
2-C. decide_send_target_good — Level 3 (3회차, 백지)
int decide_send_target_good(const char *claimed_ip, char *out_ip, size_t out_size){ /* 아무것도 안 본다. * §1 요구사항서와 §0 의 치환 규칙만 보고 친다. * * 막히면 아래만 찾아본다. * - `resolve_hostname_like(host, poisoned)` 로 DNS 오염을 흉내낸다 * - DNS 를 검증하는 게 아니라 판정 경로에서 **뺀다** */}
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다.
SYNOPSIS 맨 위의 #include 를 확인한다.
로직이 막히면 §3 테스트의 기대값을 먼저 읽는다.
공격 입력표(§1)와 테스트가 답이다.
#include <stdio.h>#include <string.h>#include "dnstrust.h"const char *resolve_hostname_like(const char *hostname, int poisoned){ if (strcmp(hostname, "www.trust.com") != 0) return NULL; return poisoned ? "198.51.100.66" : "203.0.113.10";}int decide_send_target_bad(const char *hostname, int poisoned, char *out_ip, size_t out_size){ const char *resolved; if (strcmp(hostname, "www.trust.com") != 0) return -1; /* 도메인명만 확인 */ resolved = resolve_hostname_like(hostname, poisoned); if (!resolved) return -1; snprintf(out_ip, out_size, "%s", resolved); /* DNS가 알려준 IP를 그대로 신뢰 */ return 0;}int decide_send_target_good(const char *claimed_ip, char *out_ip, size_t out_size){ if (strcmp(claimed_ip, "203.0.113.10") != 0) return -1; /* IP 화이트리스트 직접 비교 */ snprintf(out_ip, out_size, "%s", claimed_ip); return 0;}
눈여겨볼 점: decide_send_target_good의 시그니처에는 hostname도, poisoned도, resolve_hostname_like 호출도 아예 없다.
가장 확실한 치환은 “DNS 결과를 검증하는 로직 추가”가 아니라 **“DNS를 판정 경로에서 완전히 빼는 것”**이다.