
PowerShell과 Nslookup을 사용하여 DNS TXT/MX 레코드를 통해 데이터를 유출하고 페이로드를 전달하는 DNS 터널링 도구로, 제한된 언어 모드(Constrained Language Mode)와 엔드포인트 방어를 우회합니다.
Cobalt Strike DNS 비콘과 관련된 최근 작업에 영감을 받아, Microsoft Defender for Endpoint를 회피하려는 임무 목표와 함께 DNS를 사용하여 페이로드를 대상 머신으로 전송하는 방법을 조사하는 데 시간을 보냈습니다. 나아가 PowerShell이 Constrained Language Mode에 있을 때에도 가능한 방식으로 이를 수행하는 데 도전하고 싶었습니다. 이 연구는 최신 Windows 구현(예: Win10+, Server 2019+)을 대상으로 했지만, 나중에 보시겠지만 하위 버전에서도 가능할 수 있습니다.
DNS 터널링은 오랫동안 존재해 온 기술로, 다양한 공격자들이 사용해 왔습니다. 기본적으로 DNS 프로토콜을 데이터 침투/유출 수단 또는 C2 통신 채널로 사용하는 것을 의미합니다. 이 주제에 대한 자세한 내용은 많은 블로그 게시물을 참조할 수 있습니다.
이렇듯 오래되고 잘 알려진 기술이기 때문에 많은 조직에서 이를 방지하기 위한 탐지 방법을 마련해 두고 있습니다.
DNS 터널링에 사용되는 DNS 레코드 유형은 역사적으로 TXT였습니다. 그 이유는 TXT 레코드가 다른 레코드보다 더 많은 데이터를 저장할 수 있고 대소문자를 구분하기 때문입니다. 다른 레코드들은 대소문자를 구분하지 않아 인코딩에 영향을 줄 수 있습니다.
Constrained Language Mode(CLM)는 PowerShell의 제한적인 언어 모드로, PowerShell의 기능과 허용되는 기능을 크게 줄입니다. 간단히 나열하면, .NET, COM 객체, 공격자들이 선호하는 (new-object net.webclient).downloadstring... 등은 사용할 수 없습니다. 이 링크에서 더 자세한 정보를 확인할 수 있습니다. 조직은 공격 표면 축소 규칙의 일환으로 일반 사용자에게 이 정책을 적용합니다. 이는 사실상 공격자로서의 우리의 삶을 더 어렵게 만듭니다.
대부분의 사람들은 Nslookup 같은 도구를 사용하면서 DNS를 대략적으로는 알고 있을 것입니다. 기본적으로 클라이언트가 쿼리를 보내면 DNS 서버가 해당 쿼리에 대한 응답을 반환합니다. CNAME, A, AAAA, TXT, MX, NS 등 여러 종류의 DNS 레코드가 있으며, 각 레코드는 서로 다른 정보를 저장하고 반환할 수 있습니다. 이러한 레코드는 DNS 서버가 제공하는 Zonefile에 구성됩니다.
예제 zonefile은 여기에 나와 있습니다.``` $ORIGIN example.com. @ 3600 SOA ns1.p30.dynect.net. ( zone-admin.dyndns.com. ; address of responsible party 2016072701 ; serial number 3600 ; refresh period 600 ; retry period 604800 ; expire time 1800 ) ; minimum ttl 86400 NS ns1.p30.dynect.net. 86400 NS ns2.p30.dynect.net. 86400 NS ns3.p30.dynect.net. 86400 NS ns4.p30.dynect.net. 3600 MX 10 mail.example.com. 3600 MX 20 vpn.example.com. 3600 MX 30 mail.example.com. 60 A 204.13.248.106 3600 TXT "v=spf1 includespf.dynect.net ~all" mail 14400 A 204.13.248.106 vpn 60 A 216.146.45.240 webapp 60 A 216.146.46.10 webapp 60 A 216.146.46.11 www 43200 CNAME example.com.
example.com의 NS 레코드를 조회하면 ns1.p30.dynect.net, ns2.p30.dynect.net, ns3.p30.dynect.net, ns4.p30.dynect.net이 반환됩니다.
# 연구
## 도메인 이름 등록
시작하기 전에 우리가 제어하고 DNS 서버를 실행할 IP를 가리키도록 DNS 레코드를 설정하는 방법에 대해 간략히 설명해야 합니다. 아래와 같이 도메인을 구매하고 "ns1" 하위 도메인에 "dns" 하위 도메인을 가리키는 DNS 레코드를 설정했습니다. "ns1" 하위 도메인에는 서버의 공용 IP가 할당됩니다.

즉, "dns.edu....com"에 대한 모든 조회는 "ns1.edu....com"으로 전달되며, 여기에는 IP 3..86이 할당됩니다. 해당 IP에 DNS 서버를 설정하여 우리의 레코드를 제공할 것입니다. 이 부분은 나중에 다시 다루겠습니다.
## 클라이언트 측 도구 찾기
제 탐구는 간단한 구글 검색 "powershell dns module"으로 시작되었으며, [이](https://docs.microsoft.com/en-us/powershell/module/dnsclient/?view=windowsserver2022-ps) 링크가 반환되었습니다. 특히 관심을 끈 것은 Resolve-DnsName 명령어였습니다. 이는 잘 알려진 Nslookup.exe 바이너리의 PowerShell 구현체로 보입니다. 특정 유형의 레코드를 요청할 수 있습니다:

좋습니다. DNS 쿼리를 수행하고 응답을 검색할 수 있는 PowerShell 모듈이 있습니다. 제한된 언어 모드(Constrained Language Mode)에서도 작동할까요? 대답은 '그렇기도 하고 아니기도 하다'입니다.
여기서 볼 수 있듯이 새 PowerShell 창을 열고, Resolve-DnsName을 실행한 다음 PowerShell을 CLM으로 전환하고(간단한 ::WriteLine 호출로 테스트), 다시 Resolve-DnsName을 실행하면 문제 없이 작동합니다:

하지만 새 PowerShell 창을 열고 즉시 CLM으로 전환한 다음 Resolve-DnsName을 실행하려고 하면 실패합니다:

모듈이 미리 로드된 경우 CLM이 적용된 후에도 실행이 가능하지만, CLM은 모듈이 아직 로드되지 않은 경우 로드를 방지하는 것으로 보입니다. 대상 환경에서 사용자에게 CLM이 기본적으로 적용되어 있고(특정 모듈이 미리 로드되어 있는지 또는 DnsClient가 그중 하나인지 알 수 없는 경우)를 고려하여, 이 시점에서 Resolve-DnsName을 버리고 전통적인 Nslookup.exe로 돌아가기로 결정했습니다.

Nslookup.exe는 IT 도구 키트의 필수 요소이며 합법적인 목적으로 널리 사용되는 바이너리입니다. 애플리케이션 허용 목록이 문제가 되는 환경에서도 실행이 허용될 가능성이 높습니다.
Nslookup은 Resolve-DnsName 쿼리와 거의 동일한 정보를 반환하며, 적절한 시점에 약간 다르게 조작하기만 하면 됩니다.
## 실행 파일을 DNS 레코드로 변환?
좋습니다. 피해자 컴퓨터에서 DNS 쿼리를 수행할 수 있는 방법이 있습니다. Nslookup이 검색할 수 있는 형식으로 페이로드를 어떻게 제공할 수 있을까요?
실행 파일은 당연히 바이너리 파일이므로 사람이 읽을 수 없습니다. 따라서 데이터를 DNS 레코드에 넣고 Nslookup 같은 도구가 복구할 수 있는 형식으로 변환해야 합니다. 사용 가능한 인코딩 옵션은 많지만, 주요 고려 사항은 피해자 박스가 기본 Windows 도구와 CLM에서 사용 가능한 기능만으로 디코딩할 수 있는지입니다. Base64가 명백하고 자주 선택되는 답변입니다.
Base64를 사용하여 실행 파일을 거대한 사람이 읽을 수 있는 문자열로 변환한 다음, 많은 DNS 레코드로 분할하고 Nslookup을 사용하여 복구할 수 있습니다. 클라이언트 측에서는 잘 알려진 LOLBAS certutil.exe를 사용하여 수집된 DNS 레코드를 Base64 디코딩하여 바이너리 형식으로 되돌릴 수 있습니다.
이를 위해 DNS 레코드 유형에 대해 좀 더 이야기해야 합니다. 각 레코드 유형은 특정 형식으로 특정 정보를 저장합니다. 예를 들어 A 레코드는 IPv4 주소(111.111.111.111)를 저장하고 반환합니다. AAAA 레코드는 IPv6 주소를 반환하고, MX 및 NS 레코드는 도메인 이름을 반환하며, TXT 레코드는 255자 길이의 문자열을 반환할 수 있습니다. 앞서 언급했듯이 레코드 길이와 대소문자 구분 문제로 인해 TXT 레코드는 공격자에게 확실한 선택이었습니다. 더 적은 수의 레코드가 필요하고 Base64와 같은 인코딩과 호환되기 때문입니다.
이것이 어떻게 보이는지 살펴보겠습니다.
Kali VM에서 실행 파일을 Base64로 인코딩할 수 있습니다. -w 0 스위치를 사용하면 모든 개행 문자가 제거되어 한 줄의 Base64 텍스트가 됩니다:

파일을 보면 Base64가 표시됩니다:

이제 이 Base64 인코딩된 파일을 DNS TXT 레코드로 변환하여 DNS 서버에서 제공해야 합니다.
이 과정에서 배운 몇 가지 사항을 간략히 요약하겠습니다:
**1.** 단일 DNS 쿼리에 대해 여러 레코드가 반환될 때, 순서대로 반환된다는 보장이 없습니다. 이는 우리 목적에 매우 중요합니다. 모든 TXT 레코드에서 파일을 재조립해야 하며, 순서가 맞지 않으면 작동하지 않기 때문입니다.
**2.** 중복 레코드는 쿼리에 대해 반환되지 않습니다. 예를 들어 영역 파일에 3개의 TXT 레코드가 있고 그중 2개에 동일한 정보가 포함된 경우, 해당 도메인에 대해 TXT 레코드를 조회하면 고유한 레코드만 반환되므로 2개의 레코드만 반환됩니다. 순서 문제는 차치하더라도, Base64 인코딩된 페이로드에서 흔히 볼 수 있는 "AAAAA"와 같은 큰 섹션을 여러 TXT 레코드로 채워야 하는 경우, 영역 파일에 여러 개가 있더라도 "A"로 채워진 TXT 레코드 중 하나만 반환됩니다.
이러한 점을 고려하여 각 DNS 쿼리에 대해 단일 TXT 레코드만 반환되도록 해야 합니다. 여기서 하위 도메인이 등장합니다. "dns.edu...com"을 "edu....com"의 하위 도메인으로 등록한 것처럼, 추가 하위 도메인(예: 1.dns.edu....com)에 대한 레코드를 제공할 수 있습니다. 필요한 모든 TXT 레코드를 제공하기 위해 필요한 만큼 많은 하위 도메인을 만들 수 있습니다.
Base64로 인코딩된 페이로드를 살펴보겠습니다:

앞서 언급했듯이 각 TXT 레코드에 255자를 넣을 수 있습니다. 413,696 / 255 = 반올림하여 1,623입니다. 이는 많은 TXT 레코드(그리고 결과적으로 많은 하위 도메인)입니다. 하지만 시작점입니다.
Base64로 인코딩된 페이로드를 읽어 들여 영역 파일을 생성하는 Python3 스크립트를 작성했습니다:

이 스크립트는 Base64로 인코딩된 페이로드(comp.txt)를 열고 "chunkstring" 함수(스택 오버플로 게시물에서 가져옴)를 사용하여 파일을 255자 덩어리로 분할한 후 TXT 레코드를 생성합니다. 여기서 IP는 가짜/임의이며 불필요합니다.
생성된 영역 파일을 보면 TXT 레코드가 표시됩니다:

각 TXT 레코드의 맨 왼쪽에 있는 숫자는 하위 도메인을 나타냅니다.
이제 영역 파일이 생성되었으므로 DNS 서버에 복사한 다음 제공해야 합니다. 이를 위해 [CoreDNS](https://github.com/coredns/coredns)를 사용했습니다:
