고급 헌팅 쿼리 모범 사례

적용 대상:

  • Microsoft Defender XDR

이러한 권장 사항을 적용하여 결과를 더 빠르게 얻고 복잡한 쿼리를 실행하는 동안 시간 제한을 방지합니다. 쿼리 성능을 개선하는 방법에 대한 자세한 내용은 Kusto 쿼리 모범 사례를 참조하세요.

CPU 리소스 할당량 이해

크기에 따라 각 테넌트는 고급 헌팅 쿼리를 실행하기 위해 할당된 정해진 양의 CPU 리소스에 액세스할 수 있습니다. 다양한 사용 매개 변수에 대한 자세한 내용은 고급 헌팅 할당량 및 사용 매개 변수에 대해 읽어보세요.

쿼리를 실행한 후 실행 시간과 리소스 사용량(낮음, 중간, 높음)을 볼 수 있습니다. 높음은 쿼리를 실행하는 데 더 많은 리소스가 사용되었으며 결과를 보다 효율적으로 반환하도록 개선될 수 있음을 나타냅니다.

Microsoft Defender 포털의 **결과** 탭 아래의 쿼리 세부 정보

여러 쿼리를 정기적으로 실행하는 고객은 사용량을 추적하고 이 문서의 최적화 지침을 적용하여 할당량 또는 사용 매개 변수 초과로 인한 중단을 최소화해야 합니다.

KQL 쿼리 최적화를 시청하여 쿼리를 개선하는 가장 일반적인 방법 중 일부를 확인합니다.

일반 최적화 팁

  • 새 쿼리 크기 조정 - 쿼리가 큰 결과 집합을 반환할 것으로 의심되는 경우 count 연산자를 사용하여 먼저 평가합니다. 큰 결과 집합을 방지하려면 제한 또는 해당 동의어를 take 사용합니다.

  • 필터를 조기에 적용 — 특히 substring(), replace(), trim(), toupper()또는 parse_json()와 같은 변환 및 구문 분석 함수를 사용하기 전에 데이터 집합을 줄이기 위해 시간 필터 및 기타 필터를 적용합니다. 아래 예제에서는 필터링 연산자가 레코드 수를 줄인 후 구문 분석 함수 extractjson() 이 사용됩니다.

    DeviceEvents
    | where Timestamp > ago(1d)
    | where ActionType == "UsbDriveMount"
    | where DeviceName == "user-desktop.domain.com"
    | extend DriveLetter = extractjson("$.DriveLetter", AdditionalFields)
    
  • 비트 포함- 단어 내에서 부분 문자열을 불필요하게 검색하지 않도록 하려면 대신 contains연산자를 has 사용합니다. 문자열 연산자 알아보기

  • 특정 열 살펴보기 - 모든 열에서 전체 텍스트 검색을 실행하지 않고 특정 열을 찾습니다. 를 사용하여 * 모든 열을 검사 마세요.

  • 속도에 대/소문자를 구분합니다. 대/소문자를 구분하는 검색은 더 구체적이고 일반적으로 성능이 더 높습니다. 및 contains_cs와 같은 has_cs 대/소문자를 구분하는 문자열 연산자의 이름은 일반적으로 로 _cs끝납니다. 대신 대/소문자를 구분하는 equals 연산자를 ===~사용할 수도 있습니다.

  • 구문 분석, 추출하지 않음 - 가능하면 구문 분석 연산자 또는 구문 분석 함수 (예: parse_json()를 사용합니다. matches regex 정규식을 사용하는 문자열 연산자 또는 extract() 함수를 사용하지 마세요. 더 복잡한 시나리오를 위해 정규식 사용을 예약합니다. 구문 분석 함수에 대해 자세히 알아보기

  • 식이 아닌 테이블 필터링 - 테이블 열을 필터링할 수 있는 경우 계산 열을 필터링하지 마세요.

  • 3자 용어 없음 - 3자 이하의 용어를 사용하여 비교하거나 필터링하지 마세요. 이러한 용어는 인덱싱되지 않으며 일치하려면 더 많은 리소스가 필요합니다.

  • 선택적으로 프로젝트 - 필요한 열만 프로젝팅하여 결과를 더 쉽게 이해할 수 있도록 합니다. 인 또는 유사한 작업을 실행하기 전에 특정 열을 프로젝션하면 성능도 향상됩니다.

연산자 join 최적화

조인 연산자는 지정된 열의 값을 일치시켜 두 테이블의 행을 병합합니다. 이러한 팁을 적용하여 이 연산자를 사용하는 쿼리를 최적화합니다.

  • 왼쪽에 있는 작은 테이블 - 연산자는 join 조인 문의 왼쪽에 있는 테이블의 레코드를 오른쪽의 레코드와 일치합니다. 왼쪽에 작은 테이블을 두면 일치하는 레코드가 더 적어질 수 있으므로 쿼리 속도가 빨라질 수 있습니다.

    아래 표에서는 계정 SID로 조 IdentityLogonEvents 인하기 전에 세 개의 특정 디바이스만 포함하도록 왼쪽 테이블을 DeviceLogonEvents 줄입니다.

    DeviceLogonEvents
    | where DeviceName in ("device-1.domain.com", "device-2.domain.com", "device-3.domain.com")
    | where ActionType == "LogonFailed"
    | join
        (IdentityLogonEvents
        | where ActionType == "LogonFailed"
        | where Protocol == "Kerberos")
    on AccountSid
    
  • 내부 조인 버전 사용 - 기본 조인 버전 또는 innerunique-join 중복 제거는 각 일치 항목의 행을 오른쪽 테이블에 반환하기 전에 조인 키로 왼쪽 테이블의 행을 중복 제거합니다. 왼쪽 테이블에 키 값이 같은 join 행이 여러 개 있는 경우 해당 행은 중복 제거되어 각 고유 값에 대해 임의의 단일 행을 남깁니다.

    이 기본 동작은 유용한 인사이트를 제공할 수 있는 왼쪽 테이블에서 중요한 정보를 제외할 수 있습니다. 예를 들어 아래 쿼리는 여러 전자 메일 메시지를 사용하여 동일한 첨부 파일을 보낸 경우에도 특정 첨부 파일이 포함된 하나의 전자 메일만 표시합니다.

    EmailAttachmentInfo
    | where Timestamp > ago(1h)
    | where Subject == "Document Attachment" and FileName == "Document.pdf"
    | join (DeviceFileEvents | where Timestamp > ago(1h)) on SHA256
    

    이 제한을 해결하기 위해 왼쪽 테이블의 모든 행을 오른쪽에 일치하는 값으로 표시하도록 지정하여 kind=inner내부 조인 버전을 적용합니다.

    EmailAttachmentInfo
    | where Timestamp > ago(1h)
    | where Subject == "Document Attachment" and FileName == "Document.pdf"
    | join kind=inner (DeviceFileEvents | where Timestamp > ago(1h)) on SHA256
    
  • 시간 창에서 레코드 조인 - 보안 이벤트를 조사할 때 분석가는 같은 기간에 발생하는 관련 이벤트를 찾습니다. 를 사용할 join 때 동일한 접근 방식을 적용하면 검사 레코드 수를 줄여 성능이 향상됩니다.

    아래 쿼리는 악성 파일을 받은 후 30분 이내에 로그온 이벤트를 확인합니다.

    EmailEvents
    | where Timestamp > ago(7d)
    | where ThreatTypes has "Malware"
    | project EmailReceivedTime = Timestamp, Subject, SenderFromAddress, AccountName = tostring(split(RecipientEmailAddress, "@")[0])
    | join (
    DeviceLogonEvents
    | where Timestamp > ago(7d)
    | project LogonTime = Timestamp, AccountName, DeviceName
    ) on AccountName
    | where (LogonTime - EmailReceivedTime) between (0min .. 30min)
    
  • 양쪽에 시간 필터 적용 - 특정 기간을 조사하지 않더라도 왼쪽 테이블과 오른쪽 테이블에 시간 필터를 적용하면 레코드 수를 줄여 검사 성능을 향상시킬 join 수 있습니다. 아래 쿼리는 지난 1시간 동안의 레코드만 조인하도록 두 테이블에 모두 적용됩니다 Timestamp > ago(1h) .

    EmailAttachmentInfo
    | where Timestamp > ago(1h)
    | where Subject == "Document Attachment" and FileName == "Document.pdf"
    | join kind=inner (DeviceFileEvents | where Timestamp > ago(1h)) on SHA256
    
  • 성능에 힌트 사용 - 연산자에서 join 힌트를 사용하여 리소스 집약적 작업을 실행할 때 백 엔드에 부하를 분산하도록 지시합니다. 조인 힌트에 대해 자세히 알아보기

    예를 들어 순서 섞기 힌트 는 아래 쿼리의 와 같이 AccountObjectId 고유한 값이 많은 키인 카디널리티가 높은 키를 사용하여 테이블을 조인할 때 쿼리 성능을 향상시키는 데 도움이 됩니다.

    IdentityInfo
    | where JobTitle == "CONSULTANT"
    | join hint.shufflekey = AccountObjectId
    (IdentityDirectoryEvents
        | where Application == "Active Directory"
        | where ActionType == "Private data retrieval")
    on AccountObjectId
    

    브로드캐스트 힌트는 왼쪽 테이블이 작고(최대 100,000개 레코드) 오른쪽 테이블이 매우 큰 경우에 유용합니다. 예를 들어 아래 쿼리는 특정 주체가 있는 몇 개의 전자 메일을 테이블에 링크 EmailUrlInfo 가 포함된 모든 메시지와 조인하려고 합니다.

    EmailEvents
    | where Subject in ("Warning: Update your credentials now", "Action required: Update your credentials now")
    | join hint.strategy = broadcast EmailUrlInfo on NetworkMessageId
    

연산자 summarize 최적화

summarize 연산자는 테이블의 내용을 집계합니다. 이러한 팁을 적용하여 이 연산자를 사용하는 쿼리를 최적화합니다.

  • 고유 값 찾기 - 일반적으로 를 사용하여 summarize 반복할 수 있는 고유 값을 찾습니다. 반복적인 값이 없는 열을 집계하는 데 사용할 필요가 없습니다.

    단일 전자 메일은 여러 이벤트의 일부일 수 있지만, 개별 전자 메일의 네트워크 메시지 ID는 항상 고유한 보낸 사람 주소와 함께 제공되므로 아래 예제에서는 를 효율적으로 사용하지 summarize않습니다.

    EmailEvents
    | where Timestamp > ago(1h)
    | summarize by NetworkMessageId, SenderFromAddress
    

    연산자를 summarize 로 쉽게 바꿀 project수 있으며 리소스를 적게 소비하면서 잠재적으로 동일한 결과를 얻을 수 있습니다.

    EmailEvents
    | where Timestamp > ago(1h)
    | project NetworkMessageId, SenderFromAddress
    

    다음 예제에서는 동일한 받는 사람 주소로 전자 메일을 보내는 보낸 사람 주소의 여러 고유 인스턴스가 있을 수 있으므로 를 보다 효율적으로 사용하는 summarize 것입니다. 이러한 조합은 덜 고유하며 중복이 있을 수 있습니다.

    EmailEvents
    | where Timestamp > ago(1h)
    | summarize by SenderFromAddress, RecipientEmailAddress
    
  • 쿼리 순서 섞기 - 반복적인 값이 있는 열에서 가장 잘 사용되지만 summarize 동일한 열은 카디널리티가 높 거나 고유 값이 많을 수도 있습니다. 연산자와 join 마찬가지로 와 함께 summarize순서 섞기 힌트를 적용하여 처리 부하를 분산하고 카디널리티가 높은 열에서 작동할 때 성능을 향상시킬 수도 있습니다.

    아래 쿼리는 를 사용하여 summarize 대규모 조직에서 수십만 개의 고유한 수신자 전자 메일 주소를 실행할 수 있습니다. 성능을 향상시키기 위해 을 통합합니다.hint.shufflekey

    EmailEvents
    | where Timestamp > ago(1h)
    | summarize hint.shufflekey = RecipientEmailAddress count() by Subject, RecipientEmailAddress
    

쿼리 시나리오

프로세스 ID를 사용하여 고유한 프로세스 식별

PID(프로세스 ID)는 Windows에서 재활용할 수 있으며 새 프로세스를 위해 다시 사용됩니다. 즉, 특정 프로세스에 대한 고유 식별자로는 사용할 수 없습니다.

특정 컴퓨터에서 프로세스에 대한 고유 식별자를 얻으려면 프로세스 생성 시간과 함께 프로세스 ID를 사용합니다. 프로세스 주변의 데이터를 합치거나 요약할 때는 컴퓨터 식별자에 대한 열 (DeviceId 또는 DeviceName), 프로세스 ID (ProcessId 또는 InitiatingProcessId), 프로세스 만들기 시간 (ProcessCreationTime 또는 InitiatingProcessCreationTime)을 포함합니다.

다음의 예제 쿼리는 포트 445(SMB)를 통해 10 개가 넘는 IP 주소에 액세스하는 프로세스를(잠정적으로 파일 공유를 검색하며) 찾습니다.

쿼리 예제:

DeviceNetworkEvents
| where RemotePort == 445 and Timestamp > ago(12h) and InitiatingProcessId !in (0, 4)
| summarize RemoteIPCount=dcount(RemoteIP) by DeviceName, InitiatingProcessId, InitiatingProcessCreationTime, InitiatingProcessFileName
| where RemoteIPCount > 10

쿼리는 여러 프로세스를 동일한 프로세스 ID와 함께 사용하지 않고 단일 프로세스를 보여주는 InitiatingProcessIdInitiatingProcessCreationTime 모두에 대해 요약을 합니다.

쿼리 명령줄

여러 가지 방법으로 작업을 수행할 수 있는 명령줄을 만들 수 있습니다. 예를 들어 공격자는 경로 없이, 파일 확장명 없이, 환경 변수를 사용하거나 따옴표가 있는 이미지 파일을 참조할 수 있습니다. 공격자는 매개 변수의 순서를 변경하거나 여러 따옴표와 공백을 추가할 수도 있습니다.

명령줄을 중심으로 더 지속성 있는 쿼리를 만들려면 다음 방법을 적용합니다.

  • 명령줄 자체를 필터링하는 대신 파일 이름 필드에서 일치하여 알려진 프로세스(예: net.exe 또는 psexec.exe)를 식별합니다.
  • parse_command_line() 함수를 사용하여 명령줄 섹션 구문 분석
  • 명령줄 인수에 대해 쿼리할 때 관련이 없는 여러 인수에서 특정 순서로 정확하게 일치하는 항목을 찾지 마세요. 대신 정규 표현식을 사용하거나 별도의 여러 포함 연산자를 사용합니다.
  • 대/소문자를 구분하지 않는 일치 항목을 사용합니다. 예를 들어 , in~및 대신 in==, containscontains_cs를 사용합니다=~.
  • 명령줄 난독 처리 기술을 완화하려면 따옴표를 제거하고, 쉼표를 공백으로 바꾸고, 여러 개의 연속된 공백을 단일 공백으로 바꾸는 것이 좋습니다. 다른 접근 방식이 필요한 더 복잡한 난독 처리 기술이 있지만 이러한 조정은 일반적인 방법을 해결하는 데 도움이 될 수 있습니다.

다음 예제에서는 파일 net.exe 찾는 쿼리를 생성하여 방화벽 서비스 "MpsSvc"를 중지하는 다양한 방법을 보여 줍니다.

// Non-durable query - do not use
DeviceProcessEvents
| where ProcessCommandLine == "net stop MpsSvc"
| limit 10

// Better query - filters on file name, does case-insensitive matches
DeviceProcessEvents
| where Timestamp > ago(7d) and FileName in~ ("net.exe", "net1.exe") and ProcessCommandLine contains "stop" and ProcessCommandLine contains "MpsSvc"

// Best query also ignores quotes
DeviceProcessEvents
| where Timestamp > ago(7d) and FileName in~ ("net.exe", "net1.exe")
| extend CanonicalCommandLine=replace("\"", "", ProcessCommandLine)
| where CanonicalCommandLine contains "stop" and CanonicalCommandLine contains "MpsSvc"

외부 원본에서 데이터 수집

긴 목록 또는 큰 테이블을 쿼리에 통합하려면 externaldata 연산 자를 사용하여 지정된 URI에서 데이터를 수집합니다. TXT, CSV, JSON 또는 기타 형식의 파일에서 데이터를 가져올 수 있습니다. 아래 예제에서는 MalwareBazaar(abuse.ch)에서 제공하는 광범위한 맬웨어 SHA-256 해시 목록을 활용하여 전자 메일에 첨부 파일을 검사 방법을 보여 줍니다.

let abuse_sha256 = (externaldata(sha256_hash: string)
[@"https://bazaar.abuse.ch/export/txt/sha256/recent/"]
with (format="txt"))
| where sha256_hash !startswith "#"
| project sha256_hash;
abuse_sha256
| join (EmailAttachmentInfo
| where Timestamp > ago(1d)
) on $left.sha256_hash == $right.SHA256
| project Timestamp,SenderFromAddress,RecipientEmailAddress,FileName,FileType,
SHA256,ThreatTypes,DetectionMethods

문자열 구문 분석

구문 분석 또는 변환이 필요한 문자열을 효율적으로 처리하는 데 사용할 수 있는 다양한 함수가 있습니다.

String 함수 사용 예제
명령줄 parse_command_line() 명령 및 모든 인수를 추출합니다.
경로 parse_path() 파일 또는 폴더 경로의 섹션을 추출합니다.
버전 번호 parse_version() 최대 4개의 섹션과 섹션당 최대 8자로 버전 번호를 분해합니다. 구문 분석된 데이터를 사용하여 버전 나이를 비교합니다.
IPv4 주소 parse_ipv4() IPv4 주소를 긴 정수로 변환합니다. IPv4 주소를 변환하지 않고 비교하려면 ipv4_compare()를 사용합니다.
IPv6 주소 parse_ipv6() IPv4 또는 IPv6 주소를 정식 IPv6 표기법으로 변환합니다. IPv6 주소를 비교하려면 ipv6_compare()를 사용합니다.

지원되는 모든 구문 분석 함수에 대해 알아보려면 Kusto 문자열 함수에 대해 읽어보세요.

참고

이 문서의 일부 테이블은 엔드포인트용 Microsoft Defender 사용할 수 없습니다. Microsoft Defender XDR 켜서 더 많은 데이터 원본을 사용하여 위협을 헌팅합니다. 엔드포인트용 Microsoft Defender 고급 헌팅 쿼리 마이그레이션의 단계에 따라 고급 헌팅 워크플로를 엔드포인트용 Microsoft Defender Microsoft Defender XDR 이동할 수 있습니다.

더 자세히 알아보고 싶으신가요? 기술 커뮤니티: Microsoft Defender XDR Tech Community의 Microsoft 보안 커뮤니티와 Engage.