Differentiating between IoC , IoA and indicators of fraud
It’s been 2 years since I really dove into the world of Threat Hunting — and what a ride it’s been! I’ve learned so much, especially about choosing the right KQL operators when building queries (and plenty of other things too). That constant learning and improving is what keeps me excited about security. Honestly, it’s why I don’t believe anyone can truly call themselves an “expert” in the cloud — things change way too fast!
One thing I noticed from the start (and I’ve seen others struggle with it too) is how we label indicators. Not everything is an IoC, and it’s time we stop treating them like they are!
The terms IoAs, IoCs, and fraud indicators can get confusing, and it’s not always clear how they’re different or when to use them. So, let’s break it down and make sense of what each one means, how they work, and which ones matter most for keeping an organization safe.
IoA — Indicators of Attack
Indicators of Attack (IoAs) are early warning signs — like unexpected traffic spikes, suspicious logins, or abnormal user behavior — that reveal active cyber threats before they escalate. Unlike Indicators of Compromise (IoCs), which focus on forensic evidence post-breach, IoAs expose the “why” behind attacks by detecting malicious intent in real time, such as privilege escalation or unusual data transfers. This proactive approach shifts security from reacting to damage to disrupting threats mid-action. By continuously analyzing system logs, attacker tactics (TTPs), and behavioral anomalies, teams can neutralize risks before they cause harm.
For example, some IoA detections based on KQL Queries can be:
1. PowerShell.exe renamed
PowerShell is a trusted Microsoft tool that attackers can misuse by renaming its executable file to hide their actions and deliver threats. The following query, detect cli common commands to identify the mentioned executions by a renamed Powershell.
DeviceProcessEvents
| where ProcessCommandLine !contains "powershell"
| where ProcessCommandLine !contains "pwsh"
| where ProcessCommandLine contains "-NoProfile" or ProcessCommandLine contains "-ExecutionPolicy" or ProcessCommandLine contains "Invoke-Expression"
| project DeviceName, FileName,ActionType, ProcessVersionInfoOriginalFileName, ProcessCommandLine, ProcessRemoteSessionIP
2. Excessive SMTP traffic
An attacker might try to spread malware, steal sensitive info using a man-in-the-middle (MitM) attack, launch a DDoS attack, or misuse a company server to send spam and run phishing scams. The following query execute a general analysis about high volume of SMTP traffic and you can modify it to whitelist trusted domains.
let timeRange = 3d; // Adjust time window
let volumeThreshold = 500; // Alert threshold for email count
let recipientThreshold = 200; // Alert threshold for unique recipients
EmailEvents
//| where Timestamp >= ago(timeRange)
| where EmailDirection == "Outbound" and SenderFromDomain !in ("domain1","domain2")
| summarize
TotalEmails = count(),
UniqueRecipients = dcount(RecipientEmailAddress),
UniqueDomains = dcount(tostring(split(RecipientEmailAddress, "@")[1])),
TimeSpan = max(Timestamp) - min(Timestamp),
FirstActivity = min(Timestamp),
LastActivity = max(Timestamp),
SampleSubjects = makeset(Subject, 5),
SampleRecipients = makeset(RecipientEmailAddress, 5)
by SenderFromAddress, SenderMailFromAddress, SenderIPv4
| where TotalEmails > volumeThreshold
or UniqueRecipients > recipientThreshold
| project
Timestamp = LastActivity,
SenderEmail = SenderFromAddress,
SenderIPv4,
TotalEmails,
UniqueRecipients,
UniqueDomains,
TimeSpan,
SampleSubjects,
SampleRecipients,
AlertReason = case(
TotalEmails > volumeThreshold and UniqueRecipients > recipientThreshold, "High volume to many recipients",
TotalEmails > volumeThreshold, "High email volume",
UniqueRecipients > recipientThreshold, "High recipient count",
"Threshold exceeded"
)
| sort by TotalEmails desc
3. Unusual Process Execution
Attackers often use legitimate system tools — such as PowerShell, CMD, MSHTA, or WMIC — to carry out malicious operations. This technique is known as LOLBAS (Living Off the Land Binaries and Scripts). These tools are built into the OS and trusted by security controls, making them ideal for bypassing defenses.
Key Behaviors:
- Execution of base64-encoded payloads using -EncodedCommand (common in PowerShell)
- Obfuscated or suspicious commands in process lines (e.g., mshta, rundll32, regsvr32)
- Use of trusted tools to download or execute remote scripts
This query detects suspicious command-line activity by decoding base64-encoded arguments, often used in attacks. It identifies use of mshta.exe, a common technique in Lumma Stealer infections and other fileless malware, revealing hidden execution attempts even when commands are obfuscated.
DeviceFileEvents
| extend CommandWords = split(InitiatingProcessCommandLine, " ") // Split the command into words
| extend Word1 = CommandWords[0], // First word
Word2 = CommandWords[1], // Second word
Word3 = CommandWords[2], // Third word
Word4 = CommandWords[3], // Fourth word
Word5 = CommandWords[4]
| extend LongestWord = case(
strlen(Word1) >= strlen(Word2) and strlen(Word1) >= strlen(Word3) and strlen(Word1) >= strlen(Word4) and strlen(Word1) >= strlen(Word5), Word1,
strlen(Word2) >= strlen(Word1) and strlen(Word2) >= strlen(Word3) and strlen(Word2) >= strlen(Word4) and strlen(Word2) >= strlen(Word5), Word2,
strlen(Word3) >= strlen(Word1) and strlen(Word3) >= strlen(Word2) and strlen(Word3) >= strlen(Word4) and strlen(Word3) >= strlen(Word5), Word3,
strlen(Word4) >= strlen(Word1) and strlen(Word4) >= strlen(Word2) and strlen(Word4) >= strlen(Word3) and strlen(Word4) >= strlen(Word5), Word4,
Word5 // Default case if Column5 is the longest
)
| extend tostring(LongestWord)
| extend DecodedBytes = base64_decode_tostring(LongestWord)
| extend DecodedString = tostring(DecodedBytes)
| where DecodedString contains "mshta" or InitiatingProcessCommandLine contains "mshta"
| distinct DeviceName,InitiatingProcessCommandLine,LongestWord,DecodedString
Reference (Detecting Base64 Code in Commands (Inspired by the Lumma Stealer Case)
4. Command and Control (C2) Communication
Malware communicating with external servers controlled by attackers. May use uncommon ports, encrypted traffic, or frequent failed DNS lookups.
This query identifies devices into DeviceEvents table that are initiating RDP connections and provides the location of the remote IP addresses. DeviceEvents table has a column called ‘LocalIP’ which can be confusing but it also includes RemoteIPs which means Remote connections attempts from the device itself. I excluded the entries without info about the location of the IP (which means are potentially Local IPs). As optional, you can add a line to exclude “whitelisted” location such as :’ | where location !contain “Spain” ‘
DeviceEvents
| where ActionType contains "RemoteDesktopConnection"
| extend location = geo_info_from_ip_address(LocalIP)
| where location contains "Country"
| project Timestamp, DeviceName, ActionType, LocalIP, LocalPort, location,ReportId, DeviceId
IoC Indicators of Compromise
An Indicator of Compromise (IOC) is a data artifact or observable that suggests a system or network has been breached or malicious activity has occurred.
IOCs can include file hashes, IP addresses, domain names, registry changes, process anomalies, or behavioral signatures that correlate with known threats. They are typically used in forensic analysis, threat hunting, and automated detection workflows to identify, trace, and respond to security incidents post-compromise.
1. File Hashes
FileHashes are unique digital fingerprints of files (e.g., MD5, SHA256) which can be used to identify known malware or suspicious files.
These hashes serve as IOCs that can be cross-referenced with threat intelligence feeds to identify file-based threats across environments.The following KQL query joins local file events with external MD5 indicators from MalwareBazaar to surface known malicious activity.
let MalwareBazaar = externaldata(MD5: string) ["https://bazaar.abuse.ch/export/txt/md5/recent"] with (format="txt", ignoreFirstRecord=True);
let MaliciousMD5 = MalwareBazaar | where MD5 !startswith "#";
DeviceFileEvents
| join kind=inner ( MaliciousMD5) on $left.MD5 == $right.MD5
References:(Detection Response by tracing File Lineage with KQL Queries)
2. Malicious IP Addresses
Malicious IP Addresses are IOCs linked to phishing, malware delivery, or command-and-control infrastructure. Communication with these IPs often indicates a compromised user or system.
This KQL query imports the IPsum threat feed and parses it to extract IP addresses with a blacklist score (indicating how often they appear in public threat lists). It then combines identity-related logs from AADSignInEventsBeta, CloudAppEvents, and IdentityLogonEvents, and joins them with the parsed IP list. Any matches reveal activity tied to high-risk IPs, helping identify accounts interacting with known malicious infrastructure.
let ipsumrawData = externaldata(ip_string: string)[h@"https://raw.githubusercontent.com/stamparm/ipsum/master/ipsum.txt"] with (format="txt");
let parsedData = ipsumrawData
| where ip_string matches regex @"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\s+\d+$"
| extend IP = extract(@"^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", 1, ip_string), IPPubliclyBlacklist = toint(extract(@"\s+(\d+)$", 1, ip_string));
let badIPList = parsedData
| project IP, IPPubliclyBlacklist
| order by IPPubliclyBlacklist desc;
let aadSignInEventsBeta = AADSignInEventsBeta
| project Timestamp, AccountUpn, DeviceName, LogonType, IPAddress, Application, ApplicationId, ClientAppUsed, DeviceTrustType, LastPasswordChangeTimestamp, ResourceDisplayName, ResourceId, ResourceTenantId;
let cloudAppEvents = CloudAppEvents
| project Timestamp, AccountDisplayName, AccountObjectId, IPAddress, Application, ApplicationId, ActionType, ActivityType, IsAdminOperation, IsImpersonated, DeviceType, AuditSource, RawEventData, AdditionalFields, ReportId;
let identityLoginEvents = IdentityLogonEvents
| project Timestamp, AccountUpn, AccountObjectId, AccountSid, DeviceName, DeviceType, FailureReason, IPAddress, Protocol, DestinationIPAddress, DestinationPort, LogonType, AdditionalFields, ReportId;
let combinedEvents = aadSignInEventsBeta
| union cloudAppEvents
| union identityLoginEvents
| project Timestamp, AccountUpn, AccountObjectId, AccountSid, DeviceName, DeviceType, LogonType, IPAddress, Application, ActionType, ActivityType, IsAdminOperation, IsImpersonated, DeviceTrustType, LastPasswordChangeTimestamp, ResourceDisplayName, ResourceId, ResourceTenantId, FailureReason, Protocol, DestinationIPAddress, DestinationPort, AdditionalFields, ReportId;
combinedEvents
| join kind=inner (badIPList) on $left.IPAddress == $right.IP
| project Timestamp, AccountUpn, AccountObjectId, IPAddress, IPPubliclyBlacklist, LogonType, LastPasswordChangeTimestamp, Application, DeviceName, DeviceType, DeviceTrustType, ResourceDisplayName, ResourceTenantId, AdditionalFields, ResourceId, ReportId
| order by Timestamp desc;
3. Domains and URLs
Domains and URLs are among the most common IOCs used to detect and investigate malicious activity. They represent hostnames (like malicious-site.com) or full web addresses (like http://malicious-site.com/payload.exe) that are involved in:
- Phishing campaigns (e.g., fake login pages)
- Malware distribution (e.g., links to malicious payloads)
- Command-and-Control (C2) communications (e.g., backdoors, RATs)
- Initial access brokers and exploit delivery
This query pulls active malicious URLs from URLHaus and extracts their domain names. It then inspects DeviceNetworkEvents to find if any observed DNS answers match the known malicious URLs. If matched, it enriches the result with geolocation and TI context.
let URLHausOnlineRAW = externaldata (UHFeed:string) ["https://urlhaus.abuse.ch/downloads/csv_online/"] with(format="txt")
| where UHFeed !startswith "#"
| extend UHRAW=replace_string(UHFeed, '"', '')
| project splitted=split(UHRAW, ',')
| mv-expand id=splitted[0], dateadded=splitted[1], UHUrl=splitted[2], UHurl_status=splitted[3], UHlast_onlin=splitted[4], UHthreat=splitted[5], UHtags=splitted[6], UHLink=splitted[7], UHReporter=splitted[8]
| extend UHUrl = tostring(UHUrl)
| extend UHUrlDomain = tostring(parse_url(UHUrl).Host)
| project-away splitted;
DeviceNetworkEvents
| extend answers = todynamic(tostring(parse_json(AdditionalFields).answers))
| extend answersext = todynamic(tostring(parse_json(AdditionalFields).answers))
| mv-expand answers
//| extend geo_Remote_answers = todynamic(tostring(geo_info_from_ip_address(answers).country))
| extend Type =
case(
answers matches regex @"^(\d{1,3}\.){3}\d{1,3}$", "IPv4", // Matches IPv4 format
answers matches regex @"^([a-fA-F0-9:]+)$", "IPv6", // Matches IPv6 format
answers contains ".", "URL", // Checks if it contains a dot (common in URLs)
"Unknown" // Default case
)
| where Type has "URL"
| extend tostring(answers)
| join kind=inner (URLHausOnlineRAW) on $left.answers == $right.UHUrl
| extend geo_Remote_ip = tostring(geo_info_from_ip_address(RemoteIP).country)
| project Timestamp,DeviceName,LocalIP,RemoteIP,geo_Remote_ip,MaliciousAnswers = UHUrl,answersext,UHUrlDomain, ActionType
Reference: (DNS Response analysis with KQL Queries: queries, answers, TTL, RTT and others)
4. Registry Keys
Windows Registry Keys are critical system configurations stored in a hierarchical database used by the Windows OS and applications. Malicious actors often target or manipulate specific registry keys to:
- Achieve persistence (e.g., run malware at startup)
- Disable security features (e.g., real-time protection or MAPS)
- Modify system behavior for stealth or privilege escalation
This query inspects DeviceRegistryEvents for specific registry paths linked to Windows Defender policy settings. It flags cases where certain security features (e.g., MAPS, Real-Time Protection) have been disabled (RegistryValueData == 1), which may indicate tampering by an attacker or malware attempting to lower system defenses.
DeviceRegistryEvents
//If you enable these policy settings (RegistryValueData == 1), Windows Defender will not take actions or report possible threats.
//Windows Defender - Defender service itself.
//Spynet = Microsoft Active Protection Service is an online community that helps you choose how to respond to potential threats. This feature ensures the device checks in real time with the Microsoft Active Protection Service (MAPS) before allowing certain content to be run or accessed. If this feature is disabled, the check will not occur, which will lower the protection state of the device.
//Real-Time Protection = protection to scan for malware and other unwanted software. Once this has been disabled, it won’t scan anything of it.
| where RegistryKey == "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows Defender" or RegistryKey == "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\\spynet" or RegistryKey == "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Microsoft Antimalware\\Real-Time Protection"
| where RegistryValueData == 1
| distinct Timestamp, DeviceName, RegistryKey, RegistryValueName, PreviousRegistryValueData, RegistryValueData, IsInitiatingProcessRemoteSession
Reference:(KQL Advent Calendar (Day 03) Detect DefenderXDR services disabled on devices)
Indicators of Fraud
Indicators of fraud are warning signs that someone might be tricking a system for financial gain. While they can overlap with cybersecurity issues like malware, fraud indicators usually focus on actions that seem dishonest or unusual — especially involving money.
For example, imagine a credit card is used to order food in London, and just 15 minutes later, it’s used again to book a hotel room in Tokyo. Since no one can physically be in both places at once, this unusual activity could alert the system that someone may be using the card fraudulently.
1. Systems logs and alerts: Audit log irregularities, unauthorized modifications, etc.
Reveal audit log tampering, unauthorized access attempts, or suspicious configuration changes.
This KQL query below will help you detect cases where Windows Security Event Logs, has been removed directly using Event Viewer. Among the associated threats, we have:
- Lost of non-reputation evidence
- Lack of visibility over malicious activities
- Regulatory Non-Compliance
DeviceEvents
| where ActionType has "SecurityLogCleared"
2. Phishing attempts
If you get unexpected links, email attachments, or suspicious OTP messages from unknown sources, do not interact with them. They may contain malware designed to steal your personal or financial information.
The following KQL Query is based on email detected as Threats classified by ISP. If you are receiving emails from an ASN with multiple distinct IPs and all the messages tagged as a Threat… is the moment to take an action such as delete or move the email to Junk Folders.
//Sergio Albea
let CIDRASN = (externaldata (CIDR:string, CIDRASN:int, CIDRASNName:string)
['https://firewalliplists.gypthecat.com/lists/kusto/kusto-cidr-asn.csv.zip']
with (ignoreFirstRecord=true));
EmailEvents
| evaluate ipv4_lookup(CIDRASN, SenderIPv4, CIDR, return_unmatched=true)
| extend GeoIPData = tostring(geo_info_from_ip_address(SenderIPv4).country)
| summarize Different_IPs=make_set(SenderIPv4), Countries= make_set(GeoIPData), make_set(CIDR), make_set(SenderFromDomain), Total_different_IPs=dcount(SenderIPv4) ,Total_emails = count(),make_set(ThreatTypes),Delivered_on_Inbox= countif(DeliveryLocation has "Inbox/folder"), Email_Threat= count(isnotempty(ThreatTypes)),
Email_Valid = count( isempty(ThreatTypes)) by GeoIPData, CIDR, CIDRASNName
| extend SuspiciousRatio = Email_Threat * 1.0 / Total_emails, ValidRatio = Email_Valid * 1.0 / Total_emails
| extend SuspiciousPercentage = SuspiciousRatio * 100, ValidPercentage = ValidRatio * 100
| where SuspiciousPercentage > 95 and Total_different_IPs > 10
| order by Email_Threat
| project CIDRASNName,set_SenderFromDomain, set_CIDR, Different_IPs, Countries,Total_different_IPs, set_ThreatTypes,Total_emails, Delivered_on_Inbox, Email_Threat, Email_Valid, SuspiciousPercentage, ValidPercentage
3. Suspicious user behavior
Fraud can be detected through unusual actions like sudden changes to phone numbers or email addresses, unexpected transaction requests, or multiple successful sign-ins from distant countries on the same day — activities that often point to account compromise or identity theft.
The following KQL query find cases where the user success logins during the same day are from countries that are really distant. The query checks the Longitude and Latitude difference of the first 4 countries, if I have more than 4 countries I will also be notified.
let substring = ",";
AADSignInEventsBeta
| where Timestamp > ago(1d)
| where ErrorCode == 0
| where isnotempty(Country)
| project AccountUpn, Timestamp, ClientAppUsed, Country, Latitude, Longitude, ReportId, DeviceTrustType
| summarize ['Count of countries']=dcount(Country), ['List of countries']=make_set(Country), ['ListofLatitudes']=make_set(Latitude),
['ListofLongitudes']=make_set(Longitude) by AccountUpn, DeviceTrustType
| where ['Count of countries'] >= 3
// | where DeviceTrustType !contains "Azure AD registered"
| project splitted=split(ListofLatitudes, '"'),splitted1=split(ListofLongitudes, '"'), ['List of countries'], AccountUpn, ['Count of countries']
//split Latitude and transform it output (if you want to add more countries, add Lat(+1)= splitted[+2] From the last, example --> Lat5 = splitted[9] )
| mv-expand Lat1=splitted[1], Lat2=splitted[3], Lat3=splitted[5], Lat4= splitted[7]
| extend Lat1 =todouble(Lat1), Lat2 = todouble(Lat2), Lat3 = todouble(Lat3), Lat4 = todouble(Lat4)
| extend Lat1 = round(Lat1), Lat2 = round(Lat2), Lat3 = round(Lat3), Lat4 = round(Lat4)
//split Longitude and transform it output (if you want to add more countries, add Long(+1)= splitted[+2] From the last, example --> Long = splitted[9])
| mv-expand Long1=splitted1[1], Long2=splitted1[3], Long3=splitted1[5], Long4= splitted1[7]
| extend Long1 =todouble(Long1), Long2 = todouble(Long2), Long3= todouble(Long3), Long4 = todouble(Long4)
| extend Long1 = round(Long1), Long2 = round(Long2), Long3 = round(Long3), Long4 = round(Long4)
// susbstract operations
| serialize resta = Lat1 - Lat2, resta2 = Lat1 - Lat2, resta3 = Lat2 - Lat3, resta4 = Lat1 - Lat4
| serialize restal = Long1 - Long2, restal2 = Long1 - Long3, restal3 = Long2 - Long3
// Calculate the distance, add more than 15 or 20 to see more distant countries
| where (resta > 15 and resta2 > 15 and resta3> 20 and Lat1 != Lat2 and Lat1!= Lat2 and Lat2!= Lat3) or (resta < -20 and resta2 < -15 and resta3 < -15) or (restal > 20 and restal2 > 20 and restal3> 20 and Long1 != Long2 and Long1!= Long2 and Long2!= Long3) or (restal < -20 and restal2 < -20 and restal3 < -20) or (['Count of countries'] >4)
| project AccountUpn,['List of countries']
Summary
The article clarifies the differences between IoCs (evidence of past attacks), IoAs (signs an attack is happening or imminent), and fraud indicators (suspicious behaviors aimed at financial or identity manipulation).
Examples and real-world KQL queries are provided to illustrate:
- IoAs: Like PowerShell misuse, excessive SMTP traffic, or base64-encoded commands.
- IoCs: Including file hashes, malicious IPs, domains/URLs, and registry changes.
- Fraud Indicators: Sudden changes in user behavior, OTP scams, or logins from widely separated geolocations in short periods.
With each type of indicator, the article shares practical detection strategies using Microsoft Defender XDR (KQL queries) and explains their real-life relevance. It encourages threat hunters to move beyond static detections (IoCs) and proactively spot attacker behaviors and fraud before damage is done.
Differentiating between IoC , IaC and indicators of fraud was originally published in Detect FYI on Medium, where people are continuing the conversation by highlighting and responding to this story.
Introduction to Malware Binary Triage (IMBT) Course
Looking to level up your skills? Get 10% off using coupon code: MWNEWS10 for any flavor.
Enroll Now and Save 10%: Coupon Code MWNEWS10
Note: Affiliate link – your enrollment helps support this platform at no extra cost to you.
Article Link: Differentiating between IoC , IoA and indicators of fraud | by Sergio Albea | Jun, 2025 | Detect FYI