다음을 통해 공유


고급 헌팅 쿼리 모범 사례

쿼리를 최적화하여 복잡한 쿼리를 실행하는 동안 결과를 더 빠르게 얻고 시간 제한을 방지합니다. 쿼리 성능 향상에 대한 지침은 다음과 같습니다.

CPU 리소스 할당량 이해

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

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

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

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

일반 최적화 팁

  • 새 쿼리 크기 조정 - 쿼리가 큰 결과 집합을 반환할 것으로 의심되는 경우 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를 해당 프로세스 생성 시간과 디바이스 식별자(또는 DeviceName)와 결합하는 것 DeviceId 이었습니다. instance 경우 다음 예제 쿼리는 포트 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와 여러 프로세스를 혼합하지 않고 단일 프로세스를 살펴보도록 및 InitiatingProcessCreationTime 를 모두 InitiatingProcessId 요약합니다.

이 방법은 여전히 유효하며, 특히 Windows가 아닌 시스템의 경우 그렇습니다. 그러나 Windows에는 필드를 사용하는 보다 직접적인 메서드가 있습니다 ProcessUniqueId . 이전 메서드와 아래에 설명된 메서드는 모두 고유한 프로세스 인스턴스를 생성하지만, 쿼리를 간소화하고 PID 재사용 시나리오를 처리할 필요가 없으므로 사용 가능한 경우 를 사용하는 ProcessUniqueId 것이 좋습니다.

이 쿼리는 및 InitiatingProcessUniqueId 필드를 사용하여 ProcessUniqueId 특정 부모 프로세스를 자식 프로세스에 연결하는 방법을 보여 줍니다. 각 자식 InitiatingProcessUniqueId 의 를 부모의 ProcessUniqueId에 일치시켜 프로세스 ID가 시간이 지남에 따라 재사용되더라도 정확한 부모 instance 시작한 자식 프로세스만 격리합니다.

쿼리 예제:

// Step 1: Select a specific parent process instance (for instance, powershell.exe). 
let parentProcess = 
    DeviceProcessEvents
    | where FileName =~ "powershell.exe" // For your specific use case, consider modifying the FileName and adding more identifying properties to specify your query.
    | where isnotempty(ProcessUniqueId)
    | top 1 by Timestamp asc 
    | project DeviceId, DeviceName, ParentProcessUniqueId = ProcessUniqueId, ParentFileName = FileName;
// Step 2: Find all child processes started by this unique parent.
DeviceProcessEvents
| where isnotempty(InitiatingProcessUniqueId)
| join kind=inner (
    parentProcess
) on DeviceId
| where InitiatingProcessUniqueId == ParentProcessUniqueId
| project 
    DeviceName,
    ParentProcessUniqueId,
    ParentFileName,
    ChildProcessName = FileName,
    ChildProcessId = ProcessId,
    ChildProcessUniqueId = ProcessUniqueId,
    Timestamp

마찬가지로 쿼리는 동일한 프로세스 ID와 여러 프로세스를 혼합하지 않고 단일 프로세스를 살펴보도록 및 InitiatingProcessCreationTime 를 모두 InitiatingProcessId 요약합니다.

Microsoft Defender 포털에서 고유한 프로세스를 가져오기 위한 샘플 쿼리 결과의 스크린샷

쿼리 명령줄

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

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

  • 명령줄 자체를 필터링하는 대신 파일 이름 필드에서 일치하여 알려진 프로세스(예: 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 기술 커뮤니티인 Microsoft Defender XDR 기술 커뮤니티에서 Microsoft Security 커뮤니티에 참여하세요.