Category Archives: Security

Message Amplification & UDP Flooding with SIP

SIP was written to be fast and resonably lightweight.

Motorola StarTAC

At the time SIP was created in 1996, Motorola just had launched it’s first flip phone, the web was only 100,000 websites online and I was playing Pokémon.

Security wasn’t so much an afterthought, but rather not something everyone was as conscious of as they are today.

UDP is the protocol of choice for most SIP deployments, which opens it up for Message Amplification attacks.

As the world saw a few years back with DNS Amplification attacks (Good explanation of how Message Amplification works courtesy of Cloudflare), amplification attacks are enabled by DNS requests being smaller than DNS responses, and carrier networks that don’t verify the source of their traffic allowing someone to request a DNS lookup saying they’re from an IP that isn’t theirs, and that IP getting flooded.

SIP is vulnerable to this too, not exactly zero-day exploits, but something that hasn’t been looked at outside of the theoretical sphere, so I thought I’d roll up my sleeves and see how bad it can look.

For starters it’s worth remembering for a Message Amplification attack to work, it’s got to amplify. RF Engineering will teach you that amplification is the ratio of power in to power out, and it’s the same for Message Amplification, the size of the packet we send has to be smaller than the packet received, else we’re just using someone else to do our dirty work, but not amplifying.

Typical Response Sizes

These are small SIP messages I created in Python using sockets, they’re not the absolute smallest you could go, but they were as small as I could go and still get through the basic packet validation / sanity checks.

Some SIP Proxies drop traffic missing required headers while some don’t, I’ve included the required headers.

I’ve pointed the traffic at a Kamailio instance and measured the bytes sent vs bytes returned.

MethodRequest Size (bytes)Response Size (bytes)Gain
OPTIONS1682091.2x
REGISTER3804111x
INVITE1973771.9x
Content Length Mismatch339
400 Missing Required Header in Request300
Max Forwards Exceeded213

So the best we can get is a packet 1.9 times the size of the packet we put in, which means SIP isn’t the best for Message Amplification attacks, but passable, so long as it keeps responding.

INVITE gets our best amplification and we can tune this to get the request smaller.

The Workhorse

Carrier grade SIP servers are pretty powerful machines, able to handle huge amounts of traffic, quite literally hundreds of millions a day, generally split across geographic areas and clustered, all on high quality low loss, low latency IP links.

If you have 20,000 subscribers sending a keep alive every 60 seconds, you’re at 72 million dialogs consisting of two packets each (144,000,000 SIP messages).

So after some stripping down I managed to get a valid INVITE that would be responded too with an auth challenge (407 Proxy Authentication Required) which was 125 bytes on the wire, while the response was 330, giving me a gain of 2.64 times what I put in. (I send 125 bytes, I get back 330)

The Setup

We’ve got 3 IPs we’re dealing with here,

Our victim is on 10.0.1.15. UDP port 5060 won’t even be open for this poor fellow, but he’ll get flooded.

Next is our attacker who’s machine is on 10.0.1.12, but claiming their source IP is 10.0.1.15 (the Victim’s IP)

From here our attacker will be sending SIP traffic to 10.0.1.110 (our “carrier” / SIP server), which will send it’s responses to the victim. I’ve spun up an Asterisk instance because it’s the voice eng version of sticky tape, I’d love to test this against something a Broadsoft platform, but licences are hard to come by.

I setup the Asterisk instance to be single threaded, on a box with just enough resources to run, to try this small scale.

I wrote a threaded Python script that will ramp up the number of messages exponentially, we’ll start by sending one message per second, then two messages per second, and so on.

And we’ll do this until something breaks.

The Results

In short – inconclusive at first, but kinda scary after that.

Asterisk died really quickly. “Exceptionally long queue length” popped up after the first second. Interestingly, the box eventually came good and actually replied to every one of our requests, and even sent a BYE. Cute.

So I modified the script to be a bit less aggressive, a random wait time between 0 and 1 seconds between loops for each thread.

Struggling under the load.

I got about 60 seconds in before Asterisk really stopped responding to traffic.

So I tweaked my script again, enabled multi threading on the “carrier” and tried again.

So here’s the best rate (packets per second) I could get after a lot of tweaking:

Packets per Second – Red = Sent, Green = Received

Peak Receive: 14,000 packets per second
Peak Send: 20,000 packets per second

In terms of packet size – what we really care about, the results were actually pretty promising:

Click for full size

Peak receive rate of 67Mbps, for which we were putting in ~25Mbps.

So can SIP be used for message amplification attacks? Sure.

Is it particulary practical? Not really. There’s easier targets out there for the time being, so VoIP will be spared the worst of it.

But for a carrier weaponisation of carrier SIP server should be a real fear.

Protection for Carriers

Don’t use UDP for your SIP traffic.

It’s easier said than done, I know… But the reasoning for putting SIP on UDP was primarily speed and limited bandwidth, but with more and more fibre in the ground it’s no longer the case.

SIP over TCP (better yet use it as an excuse to move to TLS), will protect you from some of these attacks.

Flood protection is built into most SBCs these days, if your box is being used to hit a specific target, the source IP will be masquerading as the target. So blocking that and not responding is your best bet. Lots of SBCs still respond with a 4xx “Rate Limiting” response instead of just dropping the traffic, ideally you’d disable the nice “Rate liming” response and just drop the traffic.

Traffic modeling, GeoIP blocking and rate limiting per IP & destination port will also help, as well as monitoring.

Ultimately you can’t stop spoofed UDP traffic coming into your network, but you can stop UDP traffic leaving your network, and if everyone did that we wouldn’t be in this mess.

UDP spoofing is made possible by networks that don’t verify that the traffic that’s leaving their network is traffic that is sourced from your network.

Your core routers know what IPs are assigned to your network, and should be configured to drop traffic that’s leaving the network but not coming from those IPs.

IETF came up with this solution, and it’s built into all major router OSes:

Network Ingress Filtering:
Defeating Denial of Service Attacks which employ
IP Source Address Spoofing


Wireshark trace showing a "401 Unauthorized" Response to an IMS REGISTER request, using the AKAv1-MD5 Algorithm

All About IMS Authentication (AKAv1-MD5) in VoLTE Networks

I recently began integrating IMS Authentication functions into PyHSS, and thought I’d share my notes / research into the authentication used by IMS networks & served by a IMS capable HSS.

There’s very little useful info online on AKAv1-MD5 algorithm, but it’s actually fairly simple to understand.

RFC 2617 introduces two authentication methods for HTTP, one is Plain Text and is as it sounds – the password sent over the wire, the other is using Digest scheme authentication. This is the authentication used in standard SIP MD5 auth which I covered ages back in this post.

Authentication and Key Agreement (AKA) is a method for authentication and key distribution in a EUTRAN network. AKA is challenge-response based using symmetric cryptography. AKA runs on the ISIM function of a USIM card.

I’ve covered the AKA process in my post on USIM/HSS authentication.

The Nonce field is the Base64 encoded version of the RAND value and concatenated with the AUTN token from our AKA response. (Often called the Authentication Vectors).

That’s it!

It’s put in the SIP 401 response by the S-CSCF and sent to the UE. (Note, the Cyperhing Key & Integrity Keys are removed by the P-CSCF and used for IPsec SA establishment.

Wireshark trace showing a "401 Unauthorized" Response to an IMS REGISTER request, using the AKAv1-MD5 Algorithm
Click for Full Size version of this image

Kamailio 101 – Part 8 – Security in Practice

In our last post we went over all the theory, now let’s get started implementing these security features.

Kamailio’s core is a basis to start from, but many common needs are covered by special modules that we need to load to handle certain scenarios.

In order to authenticate traffic, we’ll need to have a source of authentication info (auth_db module) and authorization (permissions module). For this we’ll be using MySQL (although you could use text files, PostGres, etc) to store both sets of data, and using phpMyAdmin to make everything a bit more accessible.

We’ll build upon our last tutorial but we’ll install MySQL and phpMyAdmin:

apt-get install mysql-server phpmyadmin

After following along the install prompts we’ll setup our database connection.

Each of the two modules we’ll be using (auth_db and permissions) require a database source. In each we could specify our database info but instead we’ll create a new variable and fill it with our database info so we only need to update it in one place.

Let’s setup a new MySQL user for our Kamailio instance (in production you’d only grant privileges on the DB we’re going to use):

mysql> CREATE USER 'kamailio'@'localhost' IDENTIFIED BY 'my5yhtY7zPJzV8vu';

mysql> GRANT ALL PRIVILEGES ON * . * TO 'kamailio'@'localhost';

mysql> FLUSH PRIVILEGES;

We’ll now use the kamdbctl tool, bundled with Kamailio to create the database tables for us:

kamdbctl create

You’ll be asked for the root password for MySQL and if you want some optional tables (we don’t just yet) and presto, all the tables are now created!

kamdbctl - Creating database tables
Using kamdbctl

We can now login with phpMyAdmin and see the tables we just added:

Adding Database Connectivity to Kamailio

The example config is designed to be nice and modular, so by simply un-commenting the WITH_MYSQL variable and setting the DBURL variable we’ll have set our MySQL database up for the modules we need.

We’ll change:

# *** To enable mysql:
#     - define WITH_MYSQL
#

To:

# *** To enable mysql:
#!define WITH_MYSQL
#

So now we’ve defined an variabled named WITH_MYSQL

You’ll see later in the config there’s conditional (if statement) that looks at if WITH_MYSQL has been defined:

# *** Value defines - IDs used later in config
#!ifdef WITH_MYSQL
# - database URL - used to connect to database server by modules such
#       as: auth_db, acc, usrloc, a.s.o.
#!ifndef DBURL
#!define DBURL "mysql://kamailio:my5yhtY7zPJzV8vu@localhost/kamailio"
#!endif
#!endif

We’ll change the !define DBURL to include the password in the Database connection string,

It breaks up like this:

mysql:// is the database type (you could use text:// for text based DB or pgsql:// for Postgres)

First part is the username:password@host/table

In this case, our username is kamailio, our password is the one we created (my5yhtY7zPJzV8vu), our host is localhost and our table is kamailio

Adding IP Authentication & Challenge / Response Auth

Like we defined the #!define WITH_MYSQL we’ll define two other blocks to add Authentication:

# *** To enable authentication execute:
#     - enable mysql
#     - define WITH_AUTH
#     - add users using 'kamctl'
#
# *** To enable IP authentication execute:
#     - enable mysql
#     - enable authentication
#     - define WITH_IPAUTH

We’ll change to:

# *** To enable authentication execute:
#     - enable mysql
#!define WITH_AUTH
#     - add users using 'kamctl'
#
# *** To enable IP authentication execute:
#     - enable mysql
#     - enable authentication
#!define WITH_IPAUTH

Adding Users & IP Addresses

Now we’ve gone and added these blocks to the code we’ll go about adding some users and IP addresses, to do this we’ll use the kamctl tool.

kamctl is another tool (like kamcmd) used to modify / change the Kamailio config, it’s a shell wrapper for managing Kamailio database among other things.

We do need to setup a few things to get the kamctl working, and to do that we’ve got to edit the kamctlrtc file in the /etc/kamailio directory, to include the details of the database we just setup.

Editing kamctlrc
Edit the file to define our database details

Adding Users

Now we can get to work adding some users from the command line:

kamctl add 61312341234 supersecretpassword
Adding users with Kamctl

Here we can see adding a user with the username 61312341234 and the password supersecretpassword. These will makeup the username and password we’ll have on our SIP endpoints. (We’ll make them match the phone numbers of our trunks to make the routing easier down the track)

We’ll add another user so we can make calls between users when we’re testing later too, we’ll add them using kamctl add USERNAME PASSWORD again.

Now if we have a look in the subscriber table in phpMyAdmin we can see the users we created:

Kamailio Subscribers Table - Users in our databasea

Adding Carrier IPs

Next we’ll add the IP Address our carrier is going to send us calls from, so we can allow them call our users.

Kamailio’s permissions module relies on address groups – This means we could have address group (we’ll just use the number 200), which we decide is for our carriers, and add all our carrier’s IP addresses in here, without needing to put each of them into the config.

We could create another address group 300 and put the subnets of our offices in there and only allow REGISTER messages from IPs in those groups.

We’ll go through how to use the groups later on, but for now we’ll just add a new IP address to group 200, with a /32 subnet mask, on port 5060 with the name “Carrier IP address” so we know what it is.

kamctl address add 200 10.0.1.102 32 5060 "Carrier IP address"

Now if we have a look in the permissions table in Kamailio you’ll see the info we just added.

By now you’ve probably caught on to the fact kamctl is just a command line tool to add data to the database, but it’s useful none the less.

Kamailio Permissions - Address Table (Authenticated by IP Address)

The final step is to reload the permissions address table – This is done when we restart but it’s good to know you can update it without a restart.

kamctl address reload

Adding authentication / authorization to REGISTER messages

Now let’s actually put this all into practice, the first thing we’ll do is call the REQINIT route to make sure our traffic if (reasonably) clean and take care of the basics.

request_route {

        route(REQINIT);         #Call REQINIT (Request Initial) route to filter out the worst of the bad traffic and take care of the basics.

Next we’ll setup how we handle REGISTER traffic, adding an auth challenge and only saving the location if the UA successfully responds to the challenge.

if(method=="REGISTER"){  # authenticate requests
if (!auth_check("$fd", "subscriber", "1")) { #If credentials don't match what we have in Subscriber table
   auth_challenge("$fd", "0");          #Send an Auth Challenge
   exit;                                #Stop processing
 }

save("location");                   #Save the location as an AoR
exit;                               #Stop processing
        }

This may all seem a bit backward, but this is an example from the Kamailio devs we’re using, so it shows “the right way” of doing this, let’s break it down.

We know our if(method==”REGISTER”) means we’ll only do this check for REGISTER messages.

The auth_check checks to see if the presented credentials in our auth header are correct. For the first INVITE we don’t have an auth header so it’s not correct, and if we have an invalid password it’s also not correct. You’ll notice it’s prefixed with a if(!auth_check) meaning this if conditional block is only called if it fails the authentication check, and if we do fail the authentication check we issue an auth_challenge to generate a 401 response with an authentication header and send it back to the UA. Then we exit (stop processing).

As the above had an exit we know we’ll only hit blocks below if our credentials are correct, otherwise we’d just get the auth_challenge and exit from the auth_check block.

So as we know these credentials are correct we’ll save the location as an address on record using the save(“location”) function and exit.

So that’s our REGISTER block now handling & requiring authentication, now after restarting Kamailio we can register SIP devices with the username and password we setup, but if we get the username or password wrong, we’ll get rejected.

We can add extra users using the kamctl add command we touched on earlier.

Authorising / Authenticating INVITE messages

INVITE messages are used to setup sessions (calls), so it’s important we secure this too. At this point we’re authenticating to REGISTER, but not create a call (INVITE).

First let’s add a simple check to see if the INVITE has come from the IP of one of the carriers we defined earlier.

For this we’ll use the allow_source_address() command to see if the source address matches what we defined earlier using kamctl address add to address group 200 in the MySQL database.

    if(method=="INVITE"){
        if(allow_source_address("200")){        #If from a Carrier IP
            if(!lookup("location")){    #Try looking up location
                            sl_reply("404", "User not Registered");     #If looking up location fails reply with 404
                            exit;                                       #And exit
            }

            t_relay();                  #Relay traffic to endpoint
            exit();                     #Exit
        }else{
                sl_reply("403", "Nope. Don't know who you are");
                }
}

So we’ve got a simple if for if the source address is in group 200.

Presto, this works for calls from carriers coming in to registered endpoints! We can get inbound calls.

Small catch is our users can’t dial each other any more, as their IP isn’t in address group 200, they just get the 403 “Don’t know who you are” response.

Now we could go and add the subnet where our users are located, but then there’d be no point in using passwords at all. But before we do this let’s create a new routing module, called INVITE to keep everything pretty.

At the very bottom of our config we’ll add

route[ONNETINVITE]{
          if(!lookup("location")){    #Try looking up location
                            sl_reply("404", "User not Registered");     #If looking up location fails reply with 404
                            exit;                                       #And exit
            }

            t_relay();                  #Relay traffic to endpoint
            exit();                     #Exit

}

And then we’ll remove most of the code in our if(method==”INVITE”){ block and replace it with this:

if(method=="INVITE"){
 if(allow_source_address("200")){        #If from a Carrier IP
    route(ONNETINVITE);          #Call INVITE handling bloc
  }else{
    sl_reply("403", "Nope. Don't know who you are");
    }
 }

Now we’ve made it so we just call ROUTE(INVITE); when we have an INVITE we’ve authenticated. This will save us a lot of extra code when we add our checks to see if the call is from a user we recognize, instead of running through the lookup(“location”) code and relying, we’ll just call route(ONNETINVITE); when we’re happy we know who they are and off we go.

if(method=="INVITE"){
if(allow_source_address("200")){        #If from a Carrier IP
     route(ONNETINVITE);          #Call INVITE handling bloc
}else{
     if (!auth_check("$fd", "subscriber", "1")) { #If credentials don't match what we have in Subscriber table
         auth_challenge("$fd", "0");          #Send an Auth Challenge
         exit;                                #Stop processing
      }
          route(ONNETINVITE);                  #Call invite handling block
 }
}

You may recognize the !auth_check blocks as the same code we used for authenticating REGISTER messages, we’re using it again as it’s the same auth mechanism.

If we pass it we call the route(ONNETINVITE);

If we look at the packet captures we can see our INVITE gets a 407 “Proxy Authentication required” response back from Kamailio.

Kamailio - 401 & REGISTER

And the UA then resends the INVITE with an authentication header with correct username and password and we’re on our way!

And that’s it! Phew.

In production we’d want to handle other types of messages that we’d also want to authenticate, we’ll talk about that further down the line, but keep in mind every feature you add what the security ramifications might be.

Next up we’ll use our new found sense of security to add the ability to call numbers off-net (on the PSTN) via a SIP provider!

Here’s a complete copy of my running code for your reference.

Next Post – Kamailio 101 – Tutorial 9 – Adding Carrier Links | This Post – Kamailio 101 – Tutorial 8 – Security in Practice | Previous Post – Kamailio 101 – Tutorial 7 – Security in Theory| Previous Post – Kamailio 101 – Tutorial 6- Reusing Code| Kamailio 101 – Tutorial 5 – First Call| Kamailio 101 – Tutorial 4- Taking Registrations | Kamailio 101 – Tutorial 2 – Installation & First Run | Kamailio 101 – Tutorial 1 – Introduction