Python 3 and a terminal. No carrier, account or phone number is contacted, and every
number below uses a range reserved for fiction. macOS and Linux include Python; on
Windows install it from python.org with “Add python.exe to PATH”
ticked, then check with python3 --version.
If you think this is happening to you right now, go straight to step 5 — the running order at the bottom of it is the part that is time-critical, and it is deliberately not the order most people follow. The other four steps explain why it is that order.
Your Phone Number Was Never an Identity
Somewhere along the way, the phone number stopped being a way to reach you and became a way to prove you are you. Banks text codes to it. Email providers use it for password resets. Exchanges treat it as a recovery factor. The entire structure rests on an assumption that was never true: that your number can only ever be in your possession.
A SIM swap breaks that assumption directly. An attacker convinces your mobile carrier to move your number to a SIM card they control -- or to an eSIM, which requires no physical card and activates in seconds. Your phone quietly loses service. Their phone starts receiving your calls and texts, including every verification code.
Your password strength is irrelevant. The weak point is a support representative who can be socially engineered, bribed, or shown convincing stolen personal details. This is why the defense has to be applied at the carrier, before anything happens.
How the Attack Runs
Name, address, date of birth, the last four digits of a card, sometimes an account number. Most of this is available in old breach data, on social media, or from data brokers. Some is phished from you directly with a fake carrier text.
A call to support, a chat session, or a walk-in at a store with a fake ID. The story is always the same: a lost or damaged phone, and a new SIM is needed urgently.
Your phone shows "No Service" or "SOS only". Many people assume it is a network outage, and that assumption buys the attacker the time they need. If you cannot call or text but Wi-Fi still works normally, treat it as an attack until proven otherwise.
Email first, because it unlocks the rest. Then banking, exchanges, and social accounts. Each "forgot password" flow texts a code to a number they now control. The whole sequence often takes under thirty minutes.
Locking the Carrier Account
This is the part that actually prevents the attack, and it takes about ten minutes.
- Add a port-out PIN or transfer PIN -- a separate code required before your number can move to another carrier or SIM. Every major carrier offers this, often under "Number transfer PIN", "Port protection" or "Account PIN". This is the single most effective control available to you
- Turn on a SIM change lock if offered -- distinct from the port-out PIN, it blocks a SIM or eSIM swap on your existing account
- Set a separate account passcode -- not your date of birth, not your address, not anything in a breach. Treat it like a password and store it in your password manager
- Ask for an in-person identification requirement where the carrier supports it, so changes cannot be made by phone at all
- Do not use your real date of birth for security answers if the carrier allows free text -- store whatever you invent in your password manager
- Check your carrier's online account has its own MFA enabled, and that it is not SMS to the very number being protected
Removing SMS From the Critical Path
The carrier lock reduces the chance of a swap. This step reduces what a swap is worth.
- Move to passkeys or an authenticator app on every account that supports them -- especially email, banking and any exchange. An authenticator code is generated on your device and does not travel through the phone network
- Best of all, use a hardware security key for your most valuable accounts
- Remove your phone number as a recovery method where the service allows it. Adding a strong factor while leaving SMS reset enabled achieves very little
- Audit the recovery settings, not just the login settings -- the recovery path is what attackers use, and it is frequently weaker than the front door
- Consider a separate number for account recovery that you never publish -- a voice-over-IP number not tied to a SIM cannot be swapped at a carrier at all
- Reduce what is publicly linked to your number -- opt out of data brokers where practical, and avoid posting your number publicly
If SMS is the only second factor a service offers, keep it -- it defeats ordinary password reuse and credential stuffing. The point is not to abandon it, but to stop using it for the accounts that matter most, and to stop it being a reset path that bypasses everything stronger.
Recognising It in Progress
- Sudden loss of mobile service with no outage in your area, especially while Wi-Fi still works
- An unexpected text about a SIM change, a number transfer, or an account update you did not request
- Being logged out of accounts on your phone all at once
- Password reset emails you did not request, arriving in a burst
- A friend saying they received strange messages from your number
- An email confirming a change to your carrier account
The First Thirty Minutes
Speed is everything, because the attacker is working through your accounts right now. Do these in order.
Tell them explicitly that you believe your number has been fraudulently transferred and ask them to reverse it and freeze the account. Do not wait to see whether service comes back.
Your phone still has internet access even with no cellular service. Change the email password, revoke all active sessions, and remove the phone number as a recovery option. Email first, always -- it is the key to everything else.
Tell them your number is compromised and ask them to block SMS-based verification and freeze transfers pending identity checks in person.
Change passwords and revoke sessions everywhere that used SMS. Assume anything with SMS recovery has been reached.
File with your national cybercrime service -- ic3.gov in the US -- and ask
the carrier in writing for records of the change. You will need these for any dispute,
and carriers have been held liable in some jurisdictions.
Measure What Your Phone Number Is Worth to an Attacker, in Five Steps
A SIM swap is unusual among the things in this section: there is no malware, no phishing page, no password, and nothing you could have clicked differently. Somebody persuades a mobile carrier to move your number onto their SIM card, and from that moment every text message intended for you arrives on their handset instead. In the next twenty minutes you will see exactly what changes, measure how long you have, list which of your accounts are reachable through the number, and prove that a code from an authenticator app does not depend on it at all. Every line of output below came from running these files.
Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.
Do: save this as portout.py and run python3 portout.py.
"""A SIM swap is a database change at the carrier. Your phone is not involved."""
carrier_record = {
"number": "+44 7700 900412",
"subscriber": "S. Whitfield",
"active_sim": "8944 1234 5678 9012 345", # the chip in your handset
}
print("before:")
for k, v in carrier_record.items():
print(" %-12s %s" % (k, v))
# The attacker persuades a shop or a call centre that he is the subscriber.
carrier_record["active_sim"] = "8944 9999 0000 1111 222"
print()
print("after (one field changed):")
for k, v in carrier_record.items():
print(" %-12s %s" % (k, v))
print()
print("what happened to your handset :", "nothing")
print("what happened to your passwords:", "nothing")
print("what happened to your phone :", "it stops receiving calls and texts")
print("where your texts go now :", "his handset")
print()
print("No malware, no hacking, no password. One field in one database,")
print("changed by a member of staff who believed a caller.")
You should see: one field different, and everything downstream of it redirected:
before:
number +44 7700 900412
subscriber S. Whitfield
active_sim 8944 1234 5678 9012 345
after (one field changed):
number +44 7700 900412
subscriber S. Whitfield
active_sim 8944 9999 0000 1111 222
what happened to your handset : nothing
what happened to your passwords: nothing
what happened to your phone : it stops receiving calls and texts
where your texts go now : his handset
No malware, no hacking, no password. One field in one database,
changed by a member of staff who believed a caller.
This is worth dwelling on because it defeats the intuition people bring to security problems. Nothing was installed, nothing was cracked, and your phone was never touched — it simply stops receiving service, because the network now associates your number with a different chip.
The consequence is that none of your personal defences apply. The vulnerability is in a customer-service process at a company you are a customer of, and the only controls that matter are the ones that company offers you — which is step 5.
If not: this script only prints a dictionary and cannot really fail; if the two blocks look identical, the assignment between them was removed.
Go: the same folder.
Do: save this as timeline.py and run python3 timeline.py.
"""The window, in minutes. This is why the first symptom matters so much."""
EVENTS = [
(0, "the swap completes; your phone shows 'No Service'"),
(2, "attacker requests a password reset on your email"),
(3, "reset code arrives on his handset; email taken over"),
(7, "email searched for 'bank', 'crypto', 'invoice'"),
(11, "bank password reset; SMS one-time code arrives on his handset"),
(18, "transfer initiated"),
(140, "you find a shop, prove who you are, and get the number back"),
]
print("%6s %s" % ("MINUTE", "EVENT"))
print("-" * 66)
for minute, event in EVENTS:
print("%6d %s" % (minute, event))
attack_done = 18
recovery = 140
print()
print("attacker needed :", attack_done, "minutes")
print("you needed :", recovery, "minutes")
print("your margin :", attack_done - recovery, "minutes")
print()
print("'No Service' where there is normally coverage is not a network fault")
print("until you have checked. It is the only warning you get, and it")
print("arrives at minute zero.")
You should see: the attack finishing roughly two hours before you can respond:
MINUTE EVENT
------------------------------------------------------------------
0 the swap completes; your phone shows 'No Service'
2 attacker requests a password reset on your email
3 reset code arrives on his handset; email taken over
7 email searched for 'bank', 'crypto', 'invoice'
11 bank password reset; SMS one-time code arrives on his handset
18 transfer initiated
140 you find a shop, prove who you are, and get the number back
attacker needed : 18 minutes
you needed : 140 minutes
your margin : -122 minutes
'No Service' where there is normally coverage is not a network fault
until you have checked. It is the only warning you get, and it
arrives at minute zero.
The minute figures are an illustration rather than a measurement — they describe a plausible sequence, not a timed observation. What is not an illustration is the order: email first, because everything else can be reset through it, then a search of that mailbox to find out where the money is.
The practical point is the first line. Your only warning is your phone losing service, and the natural interpretation of that is “the network is playing up”. If you lose service somewhere you normally have it, and a companion standing next to you on the same network does not, that is worth two minutes of checking on wi-fi rather than two hours of waiting.
If not: if your margin prints a positive number, the subtraction was reversed
— the whole point is that it is negative by a wide margin.
Go: the same folder.
Do: save this as exposure.py and run python3 exposure.py.
"""Which of your accounts hand something to whoever holds the number?"""
ACCOUNTS = [
("email", "SMS reset", "catastrophic -- everything resets through it"),
("bank", "SMS code", "severe -- money moves"),
("crypto exchange", "SMS code", "severe -- irreversible"),
("password manager", "app + codes", "none -- no SMS route"),
("social", "SMS reset", "moderate -- impersonation, contact list"),
("work single sign-on","app + key", "none -- no SMS route"),
("shopping", "SMS reset", "minor -- saved cards"),
]
print("%-22s %-16s %s" % ("ACCOUNT", "USES THE NUMBER", "IF THE NUMBER MOVES"))
print("-" * 86)
for name, method, impact in ACCOUNTS:
print("%-22s %-16s %s" % (name, method, impact))
exposed = [a for a, m, _ in ACCOUNTS if "SMS" in m]
print()
print("accounts reachable through the number:", len(exposed), "of", len(ACCOUNTS))
print(" ", ", ".join(exposed))
print()
print("Every one of those can be moved off SMS today, in settings, for free.")
print("The two showing 'none' are not more secure by luck -- somebody chose.")
You should see: five of seven accounts reachable through one phone number:
ACCOUNT USES THE NUMBER IF THE NUMBER MOVES
--------------------------------------------------------------------------------------
email SMS reset catastrophic -- everything resets through it
bank SMS code severe -- money moves
crypto exchange SMS code severe -- irreversible
password manager app + codes none -- no SMS route
social SMS reset moderate -- impersonation, contact list
work single sign-on app + key none -- no SMS route
shopping SMS reset minor -- saved cards
accounts reachable through the number: 5 of 7
email, bank, crypto exchange, social, shopping
Every one of those can be moved off SMS today, in settings, for free.
The two showing 'none' are not more secure by luck -- somebody chose.
Rewrite this table with your own accounts and the exercise stops being theoretical. Most people find the first row is true of them — email with SMS recovery enabled — and that one row is the reason the others matter, because email is how everything else is reset.
Removing SMS recovery is usually a setting, not a purchase. Many services let you delete the phone number entirely once an authenticator app or backup codes are in place; some keep it as a fallback whether you like it or not, and those are worth knowing about, because for those accounts the carrier-side controls in step 5 are your only defence.
If not: if the exposed count is not 5, a row's middle column was reworded —
the filter matches on the text SMS appearing in it.
Go: the same folder.
Do: save this as survives.py and run python3 survives.py. The
first line checks the implementation against the test vector published in the standard itself,
so you are not taking the rest on trust.
"""Prove that app-based 2FA is not tied to your phone number."""
import base64, hashlib, hmac, struct, time
def totp(secret_b32, at):
key = base64.b32decode(secret_b32, casefold=True)
counter = struct.pack(">Q", at // 30)
digest = hmac.new(key, counter, hashlib.sha1).digest()
offset = digest[-1] & 0x0F
code = struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF
return "%06d" % (code % 1_000_000)
SECRET = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ" # what the QR code contained
# Check against the published RFC 6238 test vector before trusting anything else.
RFC_SECRET = base64.b32encode(b"12345678901234567890").decode()
print("RFC 6238 test vector at T=59:", totp(RFC_SECRET, 59), "(expected 94287082 -> last 6: 287082)")
moment = 1_700_000_000
print()
print("your old phone :", totp(SECRET, moment))
print("your new phone, ")
print(" same secret :", totp(SECRET, moment))
print()
print("phone number involved in that calculation:", "none")
print("network involved :", "none")
print()
print("A code comes from the secret plus the clock. Move the secret to a new")
print("device -- or keep it in an authenticator that syncs -- and the codes")
print("follow. Losing the NUMBER does not lose the account.")
You should see: the published vector matching, and two devices producing the same code:
RFC 6238 test vector at T=59: 287082 (expected 94287082 -> last 6: 287082)
your old phone : 921300
your new phone,
same secret : 921300
phone number involved in that calculation: none
network involved : none
A code comes from the secret plus the clock. Move the secret to a new
device -- or keep it in an authenticator that syncs -- and the codes
follow. Losing the NUMBER does not lose the account.
The first line is the important one methodologically: RFC 6238 publishes
94287082 as the correct value for that secret at that moment, and truncating it to six
digits gives 287082, which is what the code produced. An implementation that agrees
with the standard's own test vector is one you can reason from.
The rest follows: a code is a function of a secret and the clock. No network, no carrier and no phone number appear anywhere in that calculation, which is why moving the secret to a new device — or keeping it in an authenticator that syncs across your devices — keeps the account reachable even when the number is gone.
If not: if the first line does not print 287082, the secret string was altered;
it must be the base32 encoding of the twenty ASCII characters
12345678901234567890, which the script computes rather than hard-codes.
Go: the same folder.
Do: save this as lockdown.py and run python3 lockdown.py.
"""What actually stops the swap, and what to do in the first thirty minutes."""
CONTROLS = [
("a port-out PIN / transfer PIN on the carrier account", "stops it",
"ask your carrier to set one; it is separate from your voicemail PIN"),
("a 'number lock' / 'port freeze' setting", "stops it",
"in the carrier's app or account page, often off by default"),
("removing SMS as a recovery route on email", "limits it",
"the single highest-value change on this page"),
("an authenticator app or security key instead of SMS", "limits it",
"codes stop depending on the number, as step 4 proved"),
("a strong password on the phone itself", "does nothing",
"the swap happens at the carrier, not on your handset"),
("antivirus on the phone", "does nothing",
"there is no malware involved at any point"),
]
print("%-54s %-12s" % ("CONTROL", "EFFECT"))
print("-" * 74)
for control, effect, _ in CONTROLS:
print("%-54s %-12s" % (control, effect))
print()
print("controls that help :", sum(1 for _, e, _ in CONTROLS if e != "does nothing"))
print("controls that do not:", sum(1 for _, e, _ in CONTROLS if e == "does nothing"))
print()
print("IF IT IS HAPPENING NOW -- the order matters:")
FIRST_30 = [
"1. Use someone else's phone or wi-fi. Yours has no service.",
"2. Ring your bank's fraud line first. Freeze, do not explain.",
"3. Change the EMAIL password. Everything else resets through it.",
"4. Sign out of all devices on the email account.",
"5. Then ring the carrier and report the number as stolen.",
]
for line in FIRST_30:
print(" ", line)
You should see: four controls that help, two that are irrelevant, and a running order:
CONTROL EFFECT
--------------------------------------------------------------------------
a port-out PIN / transfer PIN on the carrier account stops it
a 'number lock' / 'port freeze' setting stops it
removing SMS as a recovery route on email limits it
an authenticator app or security key instead of SMS limits it
a strong password on the phone itself does nothing
antivirus on the phone does nothing
controls that help : 4
controls that do not: 2
IF IT IS HAPPENING NOW -- the order matters:
1. Use someone else's phone or wi-fi. Yours has no service.
2. Ring your bank's fraud line first. Freeze, do not explain.
3. Change the EMAIL password. Everything else resets through it.
4. Sign out of all devices on the email account.
5. Then ring the carrier and report the number as stolen.
The two “does nothing” rows are there because they are what people reach for first, and both miss entirely: the attack happens in a carrier's database, so neither the security of your handset nor any software on it is involved.
The single most valuable action is the first row. Ring your mobile provider, or open their app, and ask for a port-out PIN or number lock — a separate code required before your number can be moved to another SIM or another network. It is free, it takes a few minutes, and it converts “persuade a member of staff” into “know a secret you set”.
The running order at the bottom matters because instinct gets it wrong. People ring the carrier first, wait in a queue, and lose the email account while they are on hold. Freeze the money, then take back the email, and only then deal with the number.
If not: the counts are computed from the table, so changing a row's effect changes them
correctly; if both counts are zero, the effect strings were edited and no longer
match the literal does nothing the filter tests for.
Without scrolling up: your phone shows “No Service” in a place where you normally have full signal. Your first thought is that the network is having problems. What would you check, in what order, and what would you do differently if it turned out not to be the network? Answer: check whether it is only you: ask someone next to you on the same network, or look at whether wi-fi still works while mobile service does not — a network outage affects everyone nearby, a SIM swap affects exactly one number. If it is only you, treat it as a swap until proved otherwise, because step 2 showed the attacker's whole sequence completes in under twenty minutes while getting the number back takes hours. Then follow the order from step 5, which is deliberately not the instinctive one: use someone else's phone or a wi-fi connection, ring your bank's fraud line and freeze the accounts, change your email password and sign out of all devices on it, and only then contact the carrier. Email comes before the carrier because everything else is reset through email, and the carrier queue is where the time goes.
Now do it without the page: rewrite exposure.py with your own accounts, looking each one up rather
than remembering it, and count how many are reachable through your number. Then do the two things
the code cannot: ring your mobile provider and ask them to add a port-out PIN or number lock to
your account, and remove SMS recovery from your email if the service allows it. Those two calls
take about fifteen minutes together and close most of what you just measured.
Summary
- A phone number is a routing address, not proof of identity -- but many services still treat it as one
- The attack targets carrier support staff, so your own password strength does not help
- A port-out PIN is the highest-value ten minutes you can spend on this
- Move critical accounts off SMS to passkeys, an authenticator app, or a hardware key
- Remove SMS as a recovery path, not just as a login method
- Sudden loss of service is an alarm -- secure email over Wi-Fi first
Ask for a port-out PIN and a SIM change lock, and store the PIN in your password manager. It is one short phone call, it costs nothing, and it closes the door that this entire attack depends on.