# T1556 - Modify Authentication Process

## SOC Recommendation
Investigate Modify Authentication Process activity in the context of Defense Impairment/Persistence/Credential Access: confirm scope, affected host/identity, and whether it matches expected administrative behaviour before deciding this is benign.

## D3FEND Mappings
| D3FEND Technique | Relationship | Practical SOC Action |
|---|---|---|
| File Analysis | Detect | Monitor for File Analysis indicators relevant to this technique. |
| File Encryption | Harden | Apply File Encryption to reduce this technique's viability before an incident occurs. |
| File Eviction | Evict | Use File Eviction to remove the adversary's foothold once this technique is confirmed. |
| Content Filtering | Isolate | Apply Content Filtering to contain the blast radius once this technique is observed. |

## Investigation Steps
1. Review Defender for Endpoint / Defender XDR alerts and timeline for the affected host or identity.
2. Check Sentinel analytics rules and incidents correlated with this technique.
3. Review Entra ID sign-in and audit logs if the technique involves an identity or cloud resource.

## Evidence to Collect
- Host or resource affected
- Account or identity involved
- Timestamp of the activity
- Related process, file, or network artifact
- Any preceding or follow-on alerts

## Response Actions
| Action | Risk | Automation Safe | Approval Required |
|---|---|---|---|
| Contain the affected host or account | Medium | No | Yes |
| Collect and preserve evidence | Low | Yes | No |

## KQL
```kql
let lookback              = 1d;
let userRingThreshold     = 3;   // distinct target users sharing one phone number to call it a "burner pool"
let actorRingThreshold    = 3;   // distinct target users touched by one actor to call it "mass registration"
let TrustedRanges =
    _GetWatchlist("TrustedVPNRanges")
    | extend VPNRange = tostring(column_ifexists("SearchKey", column_ifexists("Subnet", "")))
    | where isnotempty(VPNRange)
    | extend VPNRange = trim(" ", VPNRange)
    | extend VPNRange = iff(VPNRange has "/", VPNRange, strcat(VPNRange, "/32"))
    | summarize by VPNRange;
// --- Premium-rate calling-code prefixes (analyst-maintained watchlist) ---
// "PremiumRateNumberRanges" (Analytic Rules/Watchlists/PremiumRateNumberRanges.csv)
// is seeded from two sources:
//   1. The satellite/International Network ranges (881, 882, 883, 870) that
//      are consistently documented top IRSF destinations regardless of org.
//   2. Microsoft Entra ID's own published list of country/region codes that
//      require support-ticket opt-in for MFA SMS/voice specifically due to
//      elevated telephony/IRSF fraud risk (Microsoft Learn:
//      "Telephony Fraud Protections and Throttles" / concept-mfa-regional-opt-in).
// TUNING REQUIRED before enabling: source #2 is intentionally broad (Microsoft's
// default-block posture covers ~180 countries, including ones with large
// legitimate business populations e.g. Belgium, Netherlands, Israel, Hong Kong).
// Prune the watchlist down to countries where THIS org has no legitimate users
// or customers, or false positives will be frequent. Empty/pruned watchlist
// simply yields 0 PremiumRateMatch hits - the other three signals still apply.
let premiumPrefixSet =
    toscalar(
        _GetWatchlist("PremiumRateNumberRanges")
        | extend Prefix = trim(@"[+\s]", tostring(column_ifexists("Prefix", column_ifexists("SearchKey", ""))))
        | where isnotempty(Prefix)
        | summarize make_set(Prefix)
    );
// --- Approved shared MFA numbers (e.g. help-desk break-glass line) ---
// Add E.164 numbers here that are LEGITIMATELY shared/registered across many
// accounts or providers by design, to suppress the ring/cross-provider
// signals for just that number.
let approvedSharedNumbers = dynamic([]);
// =========================== Entra ID (Microsoft) ===========================
let entraPhoneEvents =
    AuditLogs
    | where TimeGenerated >= ago(lookback)
    | where Category == "UserManagement"
    | where OperationName has_any ("security info", "Update user", "authentication method")
    | mv-expand TargetResource = TargetResources
    | mv-expand ModifiedProp = TargetResource.modifiedProperties
    | extend PropName = tostring(ModifiedProp.displayName)
    | where PropName has "phone"
    | extend NewValueRaw = tostring(ModifiedProp.newValue)
    // newValue is JSON-encoded: a quoted string ("\"+15555550123\"") or an
    // array of strings for multi-valued properties (Business Phones).
    // Strip brackets/quotes and split on comma to cover both shapes.
    | extend NewValueClean = replace_string(trim(@"[\[\]\s]", NewValueRaw), '"', "")
    | mv-expand PhoneRaw = split(NewValueClean, ",") to typeof(string)
    | where isnotempty(PhoneRaw) and PhoneRaw !in ("null", "")
    | extend ActorUPN = tolower(coalesce(tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName)))
    | extend ActorIP  = tostring(InitiatedBy.user.ipAddress)
    | extend TargetUPN = tolower(tostring(TargetResource.userPrincipalName))
    | where isnotempty(TargetUPN)
    | project TimeGenerated, Provider = "EntraID", ActorUPN, ActorIP, TargetUPN, PhoneRaw,
              EventType = OperationName, PropName;
// ================================= Okta ====================================
// See "IMPORTANT" note above - validate table/column names for your tenant.
let oktaPhoneEvents =
    union isfuzzy=true OktaSystemLog,
    (datatable(TimeGenerated:datetime, uuid:string, eventType:string, actor:dynamic, client:dynamic, target:dynamic)[])
    | where TimeGenerated >= ago(lookback)
    | where eventType has_any ("user.mfa.factor.activate", "user.mfa.factor.update", "user.factor.activate", "user.mfa.factor.enroll")
    | mv-expand Target = target
    | extend TType = tostring(Target.type), TAlt = tostring(Target.alternateId)
    // Collapse the mv-expand back to one row per source event so we can pull
    // BOTH the enrolled phone (UserFactor) and the target user (User) out of
    // the same target[] array without a row-multiplying join.
    | summarize
            PhoneRawArr  = make_set_if(TAlt, TType == "UserFactor"),
            TargetUpnArr = make_set_if(TAlt, TType == "User")
        by TimeGenerated, EventKey = coalesce(uuid, strcat(tostring(TimeGenerated), tostring(actor.alternateId))),
           eventType, ActorUPN = tolower(tostring(actor.alternateId)), ActorIP = tostring(client.ipAddress)
    | extend PhoneRawFull = tostring(PhoneRawArr[0])
    | where isnotempty(PhoneRawFull)
    // Okta factor alternateId is often prefixed, e.g. "sms:+15555550123".
    | extend PhoneRaw = extract(@'(\+?\d[\d\-\s]{5,})', 1, PhoneRawFull)
    | extend TargetUPN = tolower(tostring(TargetUpnArr[0]))
    | where isnotempty(PhoneRaw) and isnotempty(TargetUPN)
    | project TimeGenerated, Provider = "Okta", ActorUPN, ActorIP, TargetUPN, PhoneRaw,
              EventType = eventType, PropName = "phoneFactor";
// =============================== Correlate ==================================
let allEvents =
    union isfuzzy=true entraPhoneEvents, oktaPhoneEvents
    | evaluate ipv4_lookup(TrustedRanges, ActorIP, VPNRange, false)
    | extend ActorIsTrustedVPNIP = isnotempty(VPNRange)
    | extend PhoneNormalized = replace_regex(PhoneRaw, @"[^\d+]", "")
    | where isnotempty(PhoneNormalized) and PhoneNormalized != "+"
    | extend Digits = trim_start(@"\+", PhoneNormalized)
    | extend IsPremiumRate = array_length(premiumPrefixSet) > 0 and (
            set_has_element(premiumPrefixSet, substring(Digits, 0, 1))
         or set_has_element(premiumPrefixSet, substring(Digits, 0, 2))
         or set_has_element(premiumPrefixSet, substring(Digits, 0, 3))
         or set_has_element(premiumPrefixSet, substring(Digits, 0, 4)))
    | extend IsApprovedNumber = set_has_element(approvedSharedNumbers, PhoneNormalized)
    | project-away VPNRange;
// Ring signal 1: same phone number registered across multiple providers.
let crossProviderPhones =
    allEvents
    | summarize DistinctProviders = dcount(Provider), Providers = make_set(Provider, 4) by PhoneNormalized
    | where DistinctProviders >= 2;
// Ring signal 2: same phone number registered for many distinct target users.
let sharedPhones =
    allEvents
    | summarize DistinctUsersForPhone = dcount(TargetUPN), UsersForPhone = make_set(TargetUPN, 32) by PhoneNormalized
    | where DistinctUsersForPhone >= userRingThreshold;
// Ring signal 3: same actor registering MFA phones for many distinct targets.
let massRegistrationActors =
    allEvents
    | where isnotempty(ActorUPN)
    | summarize DistinctTargetsByActor = dcount(TargetUPN), TargetsByActor = make_set(TargetUPN, 32) by ActorUPN
    | where DistinctTargetsByActor >= actorRingThreshold;
allEvents
| join kind=leftouter (crossProviderPhones) on PhoneNormalized
| join kind=leftouter (sharedPhones) on PhoneNormalized
| join kind=leftouter (massRegistrationActors) on ActorUPN
| extend DistinctProviders      = coalesce(DistinctProviders, 1),
         DistinctUsersForPhone  = coalesce(DistinctUsersForPhone, 1),
         DistinctTargetsByActor = coalesce(DistinctTargetsByActor, 1)
| extend CrossProviderReuse     = DistinctProviders >= 2,
         PhoneReusedAcrossUsers = DistinctUsersForPhone >= userRingThreshold,
         ActorMassRegistration  = DistinctTargetsByActor >= actorRingThreshold
| where not(IsApprovedNumber)
| where IsPremiumRate or CrossProviderReuse or PhoneReusedAcrossUsers or ActorMassRegistration
| extend RiskScore = toint(IsPremiumRate) * 2
                    + toint(CrossProviderReuse) * 2
                    + toint(PhoneReusedAcrossUsers) * 2
                    + toint(ActorMassRegistration) * 1
| extend Signals = set_difference(
        pack_array(
            iff(IsPremiumRate,            "PremiumRateMatch",       ""),
            iff(CrossProviderReuse,       "CrossProviderReuse",     ""),
            iff(PhoneReusedAcrossUsers,   "PhoneReusedAcrossUsers", ""),
            iff(ActorMassRegistration,    "ActorMassRegistration",  "")),
        dynamic([""]))
| extend Severity = case(
        IsPremiumRate and CrossProviderReuse, "Critical",
        RiskScore >= 4,                       "Critical",
        RiskScore >= 2,                       "High",
        "Medium")
| extend AccountName = tostring(split(TargetUPN, "@")[0]),
         UPNSuffix   = tostring(split(TargetUPN, "@")[1])
| project TimeGenerated, Provider, EventType, Severity, RiskScore, Signals,
          TargetUPN, AccountName, UPNSuffix, ActorUPN, ActorIP, ActorIsTrustedVPNIP,
          PhoneRaw, PhoneNormalized, IsPremiumRate,
          DistinctProviders, CrossProviderReuse,
          DistinctUsersForPhone, PhoneReusedAcrossUsers,
          DistinctTargetsByActor, ActorMassRegistration
| extend timestamp = TimeGenerated, AccountCustomEntity = TargetUPN, IPCustomEntity = ActorIP
| order by Severity asc, RiskScore desc, TimeGenerated desc
```
```kql
AWSSecurityHubFindings
| where RecordState == "ACTIVE" and ComplianceStatus == "FAILED"
| where tostring(AwsSecurityFindingGeneratorId) == "security-control/IAM.9"
  or tostring(ComplianceSecurityControlId) == "IAM.9"
| extend RootUserARN = tostring(Resources[0].Id)
| summarize TimeGenerated = max(TimeGenerated)
    by AwsAccountId, AwsRegion, AwsSecurityFindingTitle, AwsSecurityFindingDescription,
       AwsSecurityFindingId, ComplianceSecurityControlId, RootUserARN
```
```kql
//Malicious Phishing Network Indicators - Block Recommended
let timeFrame= 5m;
CyfirmaIndicators_CL 
| where ConfidenceScore >= 80
    and TimeGenerated between (ago(timeFrame) .. now())
    and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and Roles has 'Phishing'
| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)
| mv-expand extensionKeys
| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend 
    extension_id = extensionKeyStr,
    ASN_Owner = props.asn_owner,
    ASN = props.asn,
    ProviderName = 'CYFIRMA',
    ProductName = 'DeCYFIR/DeTCT'
| project
    IPv4,
    IPv6,
    URL,
    Domain,
    ThreatActors,
    RecommendedActions,
    Sources,
    Roles,
    Country,
    IPAbuse,
    name,
    Description,
    ConfidenceScore,
    IndicatorID,
    created,
    modified,
    valid_from,
    Tags,
    ThreatType,
    TimeGenerated,
    SecurityVendors,
    ProductName,
    ProviderName
```

## Escalation Criteria
- Modify Authentication Process activity observed on a privileged account or critical system.
- Activity follows or precedes other suspicious behaviour in the same investigation.
- Automated triage cannot confidently rule out malicious intent.

## False Positive Considerations
- Legitimate administrative or maintenance activity matching this pattern.
- Approved security testing or red team exercise.
- Known benign software producing similar telemetry.

Generated by SOC Response Atlas by Basyrix.
