If you need some example pcap traces generated by any of these tools, just send an email to fasferraz@gmail.com


7/23/20

MME Part I - eNB Emulator

After years of doing applications that interact with the GGSN/PGW, I finally had the time to start and finish and old project that i had in mind for some time:

Create an eNB emulator to interact with MME (S1AP) and SGW (S1-U), once again to abstract the radio component. Something that I could use in a laptop, with connectivity to a real MME and SGW, to perform and simulate EPS Attach, TAU, PDN Connectivity Requests, and so on, and also user plane traffic.

This was done using python3, and in the following post I will talk about some of the issues I faced, and how I solved them.

So when we want to create an eNB to interact with a real network, the first concern, is that this time, I need to take into account the authentication, integrity and ciphering. When you emulate an MME or SGW to communicate with a PGW, you are bypassing all this, speaking GTP directly with the node, and filling each information element of the GTP protocol with the values you want. You can chose the IMSI, the MSISDN, the APN, the User Location, the IMEI, and so on.

With the eNB is different: if you choose an IMSI, you need to have the capability to answer with the proper XRES for the RAND/AUTN present in the Authentication Request.
But once you have that ability, you can also perform whatever procedures you want, define and change the characteristics and capabilities of the emulated terminal, and also the underlying features of the emulated eNB (like supporting 4G, 5G or NB-IoT, etc...).

So, a lot of interesting stuff can be done!


The following picture shows a simplified version of what is needed:



So as stated before, the eNB emulator has to implement S1AP and S1-U interfaces, and in case we are using a real HSS/AuC, we need also to handle the authentication and key derivation.

As I already spoke about in previous posts, I have a ZTE dongle that supports the needed AT commands (AT+CRSM and AT+CSIM) which allows me to get the CK and IK from the RAND/AUTN.

With the CK and IK and Serving Network (MCC/MNC) I can calculate the KASME, and derive the NAS keys for ciphering and integrity according to the algorithms negotiated in the Security Mode Command - So this part of the problem is solved!

Nevertheless, to simplify the process, I implemented also a simple Diameter HSS Server, that returns always the same quartet (RAND, AUTN, XRES and KASME) to the MME, so that the eNB can work without the dongle and a real HSS.

On of the issues i faced with the authentication process, was that Ubuntu was blocking my serial communication with the modem from time to time, due to the fact of the modem manager was also trying to retrieve information from the modem.
To avoid this conflict I used the answer from this post in stackoverflow: https://stackoverflow.com/questions/24696527/modem-manager-and-ttyacm-in-use
       
root@ubuntu:/home/fabricio/Documents# lsusb | grep ZTE
Bus 002 Device 000: ID 19d2:2000 ZTE WCDMA Technologies MSM MF627/MF628/MF628+/MF636+ HSDPA/HSUPA

root@ubuntu:/home/fabricio/Documents# more /lib/systemd/system/ModemManager.service 
[Unit]
Description=Modem Manager
After=polkit.service
Requires=polkit.service

[Service]
Type=dbus
BusName=org.freedesktop.ModemManager1
ExecStart=/usr/sbin/ModemManager --filter-policy=default
StandardError=null
Restart=on-abort
CapabilityBoundingSet=CAP_SYS_ADMIN
ProtectSystem=true
ProtectHome=true
PrivateTmp=true
RestrictAddressFamilies=AF_NETLINK AF_UNIX
NoNewPrivileges=true
User=root

[Install]
WantedBy=multi-user.target
Alias=dbus-org.freedesktop.ModemManager1.service


root@ubuntu:/home/fabricio/Documents# more /etc/udev/rules.d/99zte.rules 
ATTRS{idVendor}=="19d2" ATTRS{idProduct}=="0001", ENV{ID_MM_DEVICE_IGNORE}="1"
ATTRS{idVendor}=="19d2" ATTRS{idProduct}=="2000", ENV{ID_MM_DEVICE_IGNORE}="1"


root@ubuntu:/home/fabricio/Documents# udevadm control --reload-rules
   
In resume, you need to get the idVendor and idProduct from the modem using lsusb to create a new rules file for this modem with ENV{ID_MM_DEVICE_IGNORE}="1", and then change the filter-policy from strict to default in ModemManager.service, and reload the rules in the end.
After doing this no Ubuntu process tries to communicate with the modem.


One of the main challenges of building an eNB emulator, is the fact that S1AP interface uses SCTP, and is based on PER (Packed Encoding Rules) ASN.1.

I had previously done some experiments with SCTP using the native socket module from python3, but starting a ASN.1 module from scratch was a big challenge. Fortunately I found some magnificent python modules for ASN.1 and S1AP done by P1 Security that I highly recommend:


They have plenty of modules for almost anything related to Mobile Developments, but for my project i just used the S1AP from pycrate_asn1dir module, and CM from the CryptoMobile module (that has all the ciphering and integrity protocols needed for NAS). In order to derive the integrity and ciphering keys from KASME/CK/IK) i used another module: the Crypto.Hash (pip3 install pycryptodome) that has the HMAC and SHA256 functions needed for KDF.
For serial communication i use the pyserial module (pip3 install pyserial)

In resume:
       
from pycrate_asn1dir import S1AP
from pycrate_asn1rt.utils import *

from CryptoMobile.CM import *

from Crypto.Hash import HMAC
from Crypto.Hash import SHA256

import serial

 

For the usage of SCTP under S1AP, specification 36.412 has some requirements:

  1. SCTP (IETF RFC 4960 [5]) shall be supported as the transport layer of S1-MME signalling bearer. The Payload Protocol Identifier assigned by IANA to be used by SCTP for the application layer protocol S1AP is 18.
  2. There shall be only one SCTP association established between one MME and eNB pair.

  3. The eNB shall establish the SCTP association. The SCTP Destination Port number value assigned by IANA to be used for S1AP is 36412.

  4. Within the SCTP association established between one MME and eNB pair:

    -     a single pair of stream identifiers shall be reserved for the sole use of S1AP elementary procedures that utilize non UE-associated signalling.

    -     At least one pair of stream identifiers shall be reserved for the sole use of S1AP elementary procedures that utilize UE-associated signallings. However a few pairs (i.e. more than one) should be reserved.

    -     A single UE-associated signalling shall use one SCTP stream and the stream should not be changed during the communication of the UE-associated signalling.


The following code shows how to create an SCTP socket for S1AP, with Payload Protocol Identifier = 18. In this example, MME has the IP 1.1.1.1, and eNB client has the IP 2.2.2.2. The last line is to setup the variable PDU that is used to set and read the S1AP messages:
       
import socket

def main():

    server_address = ('1.1.1.1', 36412)

    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.IPPROTO_SCTP) 
    client.bind(('2.2.2.2', 0))  
    
    sctp_default_send_param = bytearray(client.getsockopt(132,10,32))
    sctp_default_send_param[11]= 18
    client.setsockopt(132, 10, sctp_default_send_param)

    client.connect(server_address)

PDU = S1AP.S1AP_PDU_Descriptions.S1AP_PDU


In order to handle the SCTP streams, I decided that I would use stream 0 for non UE associated signalling, and stream 1 for UE associated signalling.
To change the stream before sending the S1AP message over SCTP, I use the following code:

def set_stream(client, stream):    
    sctp_default_send_param = bytearray(client.getsockopt(132,10,32))
    sctp_default_send_param[0]= stream
    client.setsockopt(132, 10, sctp_default_send_param)    
    return client      


One important variable that I use across the application, is a dictionary with all the settings and parameters the application uses. This dictionary has state session variables, like ciphering and integrity keys, GUTI, M-TMSI, APNs, IP-Adresses, IMEI, NAS Message (Received/To be Sent), State information, etc... related to S1AP and NAS protocols.

This is the initialization of the dictionary. (Some keys could also be initialized through the CLI options when starting the app):
          

def session_dict_initialization(session_dict):

    session_dict['STATE'] = 0
    session_dict['ENB-UE-S1AP-ID'] = 1000
    session_dict['ENB-NAME'] = 'Fabricio-eNB'
    session_dict['ENB-PLMN'] = return_plmn(PLMN)
    session_dict['XRES'] = b'xresxres'

    session_dict['KASME'] = b'kasme   kasme   kasme   kasme   '
    # hex: 6b61736d652020206b61736d652020206b61736d652020206b61736d65202020
    
    session_dict['ENB-GTP-ADDRESS-INT'] = ''
    
    session_dict['RAB-ID'] = []
    session_dict['SGW-GTP-ADDRESS'] = []
    session_dict['SGW-TEID'] = []
    
    session_dict['EPS-BEARER-IDENTITY'] = []
    session_dict['EPS-BEARER-TYPE'] = []  # default 0, dedicated 1
    session_dict['EPS-BEARER-STATE']  = [] # active 1, inactive 0
    session_dict['EPS-BEARER-APN'] = []
    session_dict['PDN-ADDRESS'] = []

    session_dict['PDN-ADDRESS-IPV4'] = None
    session_dict['PDN-ADDRESS-IPV6'] = None
    
    session_dict['ENB-TAC'] = b'\x00\x01'
    session_dict['ENB-TAC-NBIOT'] = b'\x00\x02'    
    session_dict['ENB-ID'] = 1
    session_dict['ENB-CELLID'] = 1000000
    
    session_dict['NAS-KEY-EEA1'] = return_key(session_dict['KASME'],1,'NAS-ENC')
    session_dict['NAS-KEY-EEA2'] = return_key(session_dict['KASME'],2,'NAS-ENC')
    session_dict['NAS-KEY-EEA3'] = return_key(session_dict['KASME'],3,'NAS-ENC')
    session_dict['NAS-KEY-EIA1'] = return_key(session_dict['KASME'],1,'NAS-INT')
    session_dict['NAS-KEY-EIA2'] = return_key(session_dict['KASME'],2,'NAS-INT')
    session_dict['NAS-KEY-EIA3'] = return_key(session_dict['KASME'],3,'NAS-INT')  
    session_dict['UP-COUNT'] = -1    
    session_dict['DOWN-COUNT'] = -1
  
    session_dict['ENC-ALG'] = 0
    session_dict['INT-ALG'] = 0 
    session_dict['ENC-KEY'] = None
    session_dict['INT-KEY'] = None  
    session_dict['APN'] = APN
    
    
    session_dict['NAS-SMS-MT'] = None
    
    if session_dict['LOCAL_KEYS'] == True:
        if session_dict['IMSI'] == None:
            session_dict['IMSI'] = IMSI
        
    else:
        if session_dict['IMSI'] == None:
            try:
            
                session_dict['IMSI'] = get_imsi(session_dict['SERIAL-INTERFACE'])
            except:
                session_dict['LOCAL_KEYS'] = True
                session_dict['IMSI'] = IMSI
        
    if session_dict['IMEISV'] == None:
        session_dict['IMEISV'] = IMEISV
    
    session_dict['ENCODED-IMSI'] = eNAS.encode_imsi(session_dict['IMSI'])
    session_dict['ENCODED-IMEI'] = eNAS.encode_imei(IMEISV)
    session_dict['ENCODED-GUTI'] = eNAS.encode_guti(12345,32769,1,12345678)
    
    session_dict['S-TMSI'] = None
    
    session_dict['TMSI'] = None
    session_dict['LAI'] = None
    
    session_dict['CPSR-TYPE'] = 0
    
    session_dict['S1-TYPE'] = "4G"
    session_dict['MOBILE-IDENTITY'] = session_dict['ENCODED-IMSI'] 
    session_dict['MOBILE-IDENTITY-TYPE'] = "IMSI" 
    session_dict['SESSION-SESSION-TYPE'] = None
    session_dict['SESSION-TYPE'] = "4G"
    session_dict['SESSION-TYPE-TUN'] = 1
    session_dict['PDP-TYPE'] = 1
    session_dict['ATTACH-PDN'] = None
    session_dict['ATTACH-TYPE'] = 1
    session_dict['TAU-TYPE'] = 0
    session_dict['SMS-UPDATE-TYPE'] = False
    session_dict['NBIOT-SESSION-TYPE'] = "NONE"
    session_dict['CPSR-TYPE'] = 0

    session_dict['UECONTEXTRELEASE-CSFB'] = False
    
    session_dict['PROCESS-PAGING'] = True

    session_dict['LOG'] = []

    return session_dict
  
 

So, a lot of different settings and variables inside this dictionary!

The first step after being connected to the MME is sending the non-UE related S1SetupRequest message to bring-up the eNB.

The pycrate module uses a very python-way of creating the ASN.1 messages for S1AP.

It's not straightforward, but once you get into it, it becomes more easier.
Of course you need to be good with ASN.1, at least reading it properly in the 3GPP 36.413 specification! 😅 

You need to know the exact names of information elements!

This is what a S1SetupRequest message looks like:
       

def S1SetupRequest(dic):

    IEs = []
    IEs.append({'id': 59, 'value': ('Global-ENB-ID', {'pLMNidentity': dic['ENB-PLMN'], 'eNB-ID' : ('macroENB-ID', (dic['ENB-ID'], 20))}), 'criticality': 'reject'})
    IEs.append({'id': 60, 'value': ('ENBname', dic['ENB-NAME']), 'criticality': 'ignore'})    
    if dic['S1-TYPE'] == "4G" :
        IEs.append({'id': 64, 'value': ('SupportedTAs', [{'tAC': dic['ENB-TAC'], 'broadcastPLMNs': [dic['ENB-PLMN']]}]), 'criticality': 'reject'})    
    elif dic['S1-TYPE'] == "NBIOT":
        IEs.append({'id': 64, 'value': ('SupportedTAs', [{'tAC': dic['ENB-TAC-NBIOT'], 'broadcastPLMNs': [dic['ENB-PLMN']], 'iE-Extensions': [{'id':232, 'criticality': 'reject', 'extensionValue':('RAT-Type','nbiot')}]}]), 'criticality': 'reject'})        
    elif dic['S1-TYPE'] == "BOTH":
        IEs.append({'id': 64, 'value': ('SupportedTAs', [{'tAC': dic['ENB-TAC'], 'broadcastPLMNs': [dic['ENB-PLMN']]}, {'tAC': dic['ENB-TAC-NBIOT'], 'broadcastPLMNs': [dic['ENB-PLMN']], 'iE-Extensions': [{'id':232, 'criticality': 'reject', 'extensionValue':('RAT-Type','nbiot')}]}]), 'criticality': 'reject'})        
    IEs.append({'id': 137, 'value': ('PagingDRX', 'v128'), 'criticality': 'ignore'})
    if dic['S1-TYPE'] == "NBIOT" or dic['S1-TYPE'] == "BOTH":
        IEs.append({'id': 234, 'value': ('NB-IoT-DefaultPagingDRX', 'v256'), 'criticality': 'ignore'})  
    val = ('initiatingMessage', {'procedureCode': 17, 'value': ('S1SetupRequest', {'protocolIEs': IEs}), 'criticality': 'ignore'})
    dic = eMENU.print_log(dic, "S1AP: sending S1SetupRequest")
    return val


       
 

To send this S1SetupRequest to the MME, I use this code:
       
    PDU.set_val(S1SetupRequest(session_dict))
    message = PDU.to_aper()
    client = set_stream(client, 0)        
    bytes_sent = client.send(message)

The first line puts the list in the PDU variable, and the second line transform that into bytes in PER ASN.1 format according to 36.413.
The third line sets the stream to 0, because we are sending this message in the non UE strream, and the forth line is just sending the bytes out through the SCTP socket towards the MME.


The following code is the one I use to handle all S1AP messages received by the application, and that the application currently supports.

The sub functions that are called for each one of theses messages types are not shown, except the last function that is the one used to process the S1SetupResponse (in case of successfullOutcome), to retrieve the relevant MME information:
       
def ProcessS1AP(PDU, client, session_dict):

    buffer = client.recv(4096)
    
    PDU.from_aper(buffer)
    
    (type, pdu_dict) = PDU()
    
    if type == 'initiatingMessage':
        procedure, protocolIEs_list = pdu_dict['value'][0], pdu_dict['value'][1]['protocolIEs']
        
        #Non UE Related:
        if procedure == 'MMEConfigurationUpdate':
            session_dict = eMENU.print_log(session_dict, "S1AP: MMEConfigurationUpdate received")
            answer, session_dict = MMEConfigurationUpdateAcknowledge(protocolIEs_list, session_dict)
            PDU.set_val(answer)
            message = PDU.to_aper()
            client = set_stream(client, 0)
            bytes_sent = client.send(message)
            client = set_stream(client, 1)
        
        #UE Related:
        elif procedure == 'DownlinkNASTransport':
            session_dict = eMENU.print_log(session_dict, "S1AP: DownlinkNASTransport received")
            answer_list, session_dict = ProcessDownlinkNASTransport(protocolIEs_list, session_dict)
            for answer in answer_list:
                if answer != None:
                    PDU.set_val(answer)
                    message = PDU.to_aper()               
                    bytes_sent = client.send(message)
        
        elif procedure == 'InitialContextSetupRequest':
            session_dict = eMENU.print_log(session_dict, "S1AP: InitialContextSetupRequest received")
            answer_list, session_dict= ProcessInitialContextSetupRequest(protocolIEs_list, session_dict)
            for answer in answer_list:
                if answer != None:
                    PDU.set_val(answer)
                    message = PDU.to_aper()               
                    bytes_sent = client.send(message)                     
                
        elif procedure == 'UEContextReleaseCommand':
            session_dict = eMENU.print_log(session_dict, "S1AP: UEContextReleaseCommand received")
            answer, session_dict = ProcessUEContextReleaseCommand(protocolIEs_list, session_dict)
            if answer != None:
                PDU.set_val(answer)
                message = PDU.to_aper()               
                bytes_sent = client.send(message)    
                
        elif procedure == 'Paging':   
            if session_dict['PROCESS-PAGING'] == True:        
                session_dict = eMENU.print_log(session_dict, "S1AP: Paging received")
                answer, session_dict = ProcessPaging(protocolIEs_list, session_dict)
                if answer != None:
                    PDU.set_val(answer)
                    message = PDU.to_aper()               
                    bytes_sent = client.send(message) 

        elif procedure == 'E-RABSetupRequest':
            session_dict = eMENU.print_log(session_dict, "S1AP: ERABSetupRequest received")            
            answer_list, session_dict = ProcessERABSetupRequest(protocolIEs_list, session_dict)
            for answer in answer_list:
                if answer != None:
                    PDU.set_val(answer)
                    message = PDU.to_aper()               
                    bytes_sent = client.send(message)                           

        elif procedure == 'E-RABReleaseCommand':
            session_dict = eMENU.print_log(session_dict, "S1AP: ERABReleaseCommand received")            
            answer, session_dict, answer2 = ProcessERABReleaseCommand(protocolIEs_list, session_dict)
            if answer != None:
                PDU.set_val(answer)
                message = PDU.to_aper()               
                bytes_sent = client.send(message)                 
            if answer2 != None: #if also uplinkNAStransport message needed
                PDU.set_val(answer2)
                message = PDU.to_aper()               
                bytes_sent = client.send(message)   
  
        elif procedure == 'LocationReportingControl':
            session_dict = eMENU.print_log(session_dict, "S1AP: LocationReportingControl received")            
            answer, session_dict = ProcessLocationReportingControl(protocolIEs_list, session_dict)
            if answer != None:
                PDU.set_val(answer)
                message = PDU.to_aper()               
                bytes_sent = client.send(message)           

        elif procedure == 'UEContextModificationRequest':
            session_dict = eMENU.print_log(session_dict, "S1AP: UEContextModificationRequest received") 
            answer_list, session_dict = ProcessUEContextModificationRequest(protocolIEs_list, session_dict)
            for answer in answer_list:
                if answer != None:
                    PDU.set_val(answer)
                    message = PDU.to_aper()               
                    bytes_sent = client.send(message)               

        else:
            session_dict = eMENU.print_log(session_dict, "S1AP: " + procedure + " received") 
             
    elif type == 'successfulOutcome':
        procedure, protocolIEs_list = pdu_dict['value'][0], pdu_dict['value'][1]['protocolIEs']
        if procedure == "S1SetupResponse":
            session_dict = eMENU.print_log(session_dict, "S1AP: S1SetupResponse received")
            session_dict = S1SetupResponseProcessing(protocolIEs_list, session_dict)
        
        elif procedure == "ResetAcknowledge":
            session_dict = eMENU.print_log(session_dict, "S1AP: ResetAcknowledge received")
        else:
            session_dict = eMENU.print_log(session_dict, "S1AP: " + procedure + " received") 
    elif type == 'unsuccessfulOutcome':
        
        exit(1)

    return PDU, client, session_dict


def S1SetupResponseProcessing(IEs, dic):

mme_name = '' servedPLMNs = b'' servedGroupIDs = b'' servedMMECs = b'' RelativeMMECapacity = 0 for i in IEs: if i['id'] == 61: mme_name = i['value'][1] elif i['id'] == 105: servedPLMNs = i['value'][1][0]['servedPLMNs'][0] servedGroupIDs = i['value'][1][0]['servedGroupIDs'][0] servedMMECs = i['value'][1][0]['servedMMECs'][0] elif i['id'] == 87: RelativeMMECapacity = i['value'][1] dic['MME-NAME'] = mme_name dic['MME-PLMN'] = servedPLMNs dic['MME-GROUP-ID'] = servedGroupIDs dic['MME-CODE'] = servedMMECs dic['MME-RELATIVE-CAPACITY'] = RelativeMMECapacity dic['STATE'] = 1 return dic


In terms of NAS messages, I wrote a module for decoding and encoding of all the NAS messages of 3GPP 24.301.

This is just the encoding part. I show one example of usage, for the AttachRequest, that uses also the PDNConnectivityRequest:

def nas_encode(nas_list):    
    nas = b''
    protocol_discriminator = nas_list[0][0]
    security_header = nas_list[0][1]
    nas += bytes([(security_header*16)+protocol_discriminator])
    for i in range(1,len(nas_list)):
        if nas_list[i][0] == 0:
            if nas_list[i][1] == 'V':
                nas += nas_list[i][2]
            elif nas_list[i][1] == 'LV':
                nas += bytes([len(nas_list[i][2])]) + nas_list[i][2]
            elif nas_list[i][1] == 'LV-E':
                nas += bytes([len(nas_list[i][2])//256]) + bytes([len(nas_list[i][2])%256]) + nas_list[i][2]
        else:
            if nas_list[i][1] == "TV":
                if nas_list[i][0] < 16: 
                    nas += bytes([(nas_list[i][0]*16) + nas_list[i][2]])
                else:
                    nas += bytes([nas_list[i][0]]) + nas_list[i][2]
            if nas_list[i][1] == "TLV":
                nas += bytes([nas_list[i][0]]) + bytes([len(nas_list[i][2])]) + nas_list[i][2]    
            if nas_list[i][1] == "TLV-E":
                nas += bytes([nas_list[i][0]]) + bytes([len(nas_list[i][2])//256]) + bytes([len(nas_list[i][2])%256]) + nas_list[i][2]     
    
    
    return nas



def nas_attach_request(type, esm_information_transfer_flag, eps_identity, pdp_type, attach_type, tmsi, lai, sms_update):
    
    emm_list = []
    emm_list.append((7,0))  # protocol discriminator  
    emm_list.append((0,'V',bytes([65]))) # message type: attach request
    emm_list.append((0,'V',bytes([attach_type])))   # eps attach type / nas key set identifier   
    emm_list.append((0,'LV',eps_identity))  # eps mobile identity

    if type[0] == "4G":
        emm_list.append((0,'LV',unhexlify('f0f0c04009')))
    elif type[0] == "NBIOT":
        emm_list.append((0,'LV',unhexlify('f0f0000008a4')))
    elif type[0] == "5G":
        emm_list.append((0,'LV',unhexlify('f0f0c0c0000010')))

    
    emm_list.append((0,'LV-E',nas_pdn_connectivity(0,1,pdp_type,None,None,esm_information_transfer_flag)))
    
    if type[0] == "4G":
        if attach_type == 2 and lai != None:
            emm_list.append((0x13, 'TV', lai))
        if attach_type == 2 and tmsi == None:
            emm_list.append((0x9, 'TV', 0))     
        
        if sms_update == True:
            emm_list.append((0xF, 'TV', 1))
        
        if attach_type == 2 and tmsi != None:
            emm_list.append((0x10, 'TLV', tmsi[-3:-2] + bytes([(tmsi[-2]//64)*64])))    
            
                
    elif type[0] == "NBIOT":
        if attach_type == 2 and lai != None:
            emm_list.append((0x13, 'TV', lai))
        if attach_type == 2 and tmsi == None:
            emm_list.append((0x9, 'TV', 0))  
        if sms_update == True:
            emm_list.append((0xF, 'TV', 5))
        else:
            emm_list.append((0xF, 'TV', 4))
        emm_list.append((0xC, 'TV', 1))

        if attach_type == 2 and tmsi != None:
            emm_list.append((0x10, 'TLV', tmsi[-3:-2] + bytes([(tmsi[-2]//64)*64]))) 
            
        if type[1] == "PSM" or type[1] == "BOTH":
            emm_list.append((0x6A, 'TLV', b'\x0f')) # 15*2=30 sec.
        
        emm_list.append((0x5E, 'TLV', b'\x41'))
        if type[1] == "EDRX" or type[1] == "BOTH":
            emm_list.append((0x6E, 'TLV', b'\x75'))
        
    elif type[0] == "5G":
        if attach_type == 2 and lai != None:
            emm_list.append((0x13, 'TV', lai))
        if attach_type == 2 and tmsi == None:
            emm_list.append((0x9, 'TV', 0)) 
        if sms_update == True:
            emm_list.append((0xF, 'TV', 1))
        if attach_type == 2 and tmsi != None:
            emm_list.append((0x10, 'TLV', tmsi[-3:-2] + bytes([(tmsi[-2]//64)*64])))             
        emm_list.append((0x6F, 'TLV', b'\xf0\x00\xf0\x00'))
    
    
    
    return eNAS.nas_encode(emm_list)
    

def nas_pdn_connectivity(eps_bearer_identity, pti, pdp_type, apn, pco, esm_information_transfer_flag):
    esm_list = []
    esm_list.append((2,eps_bearer_identity))   # protocol discriminator / eps bearer identity
    esm_list.append((0,'V',bytes([pti]))) # procedure trnasaction identity
    esm_list.append((0,'V',bytes([208]))) # message type: pdn connectivity request
    esm_list.append((0,'V',bytes([(pdp_type*16) + 1])))

    if esm_information_transfer_flag != None:
        esm_list.append((0xD,'TV',esm_information_transfer_flag)) 
    if apn != None:
        esm_list.append((0x28,'TLV',apn)) 
    if pco != None:
        esm_list.append((0x27,'TLV',pco)) 

    return eNAS.nas_encode(esm_list) 

    
    

To send an NAS AttachRequest we need to use the S1AP InitialUEMessage. This is the code I use (it uses the dictionary; the AttachRequest bytes need to be already in the dictionary key 'NAS'):
       

def InitialUEMessage(dic):
    
    IEs = []
    IEs.append({'id': 8, 'value': ('ENB-UE-S1AP-ID', dic['ENB-UE-S1AP-ID']), 'criticality': 'reject'})
    IEs.append({'id': 26, 'value': ('NAS-PDU', dic['NAS']), 'criticality': 'reject'})

    if dic['SESSION-TYPE'] == "4G" or dic['SESSION-TYPE'] == "5G":
        IEs.append({'id': 67, 'value': ('TAI', {'pLMNidentity': dic['ENB-PLMN'], 'tAC': dic['ENB-TAC']}), 'criticality': 'reject'})
    elif dic['SESSION-TYPE'] == "NBIOT":
        IEs.append({'id': 67, 'value': ('TAI', {'pLMNidentity': dic['ENB-PLMN'], 'tAC': dic['ENB-TAC-NBIOT']}), 'criticality': 'reject'})        
        
    IEs.append({'id': 100, 'value': ('EUTRAN-CGI', {'cell-ID': (dic['ENB-CELLID'], 28), 'pLMNidentity': dic['ENB-PLMN']}), 'criticality': 'ignore'})
    IEs.append({'id': 134, 'value': ('RRC-Establishment-Cause', 'mo-Signalling'), 'criticality': 'ignore'})
    if dic['S-TMSI'] != None:
        IEs.append({'id': 96, 'value': ('S-TMSI', {'mMEC': dic['S-TMSI'][0:1], 'm-TMSI': dic['S-TMSI'][1:5]}), 'criticality': 'reject'})
 
    val = ('initiatingMessage', {'procedureCode': 12, 'value': ('InitialUEMessage', {'protocolIEs': IEs}), 'criticality': 'ignore'})
    dic = eMENU.print_log(dic, "S1AP: sending InitialUEMessage")

    return val




All the other NAS and S1AP messages follow the same structure. When the integrity and ciphering is active you need to add the MAC and encrypt the NAS messages, before using it in the corresponding S1AP.

[Note: Not all messages need to be encrypted or integrity protected. You should read 3GPP 24.301 to know exactly, and under which conditions, that should happen.]

When you receive an encrypted NAS, the process is the reverse: First decrypt it, then decoded it, analyse what message is and what actions need to be taken (if any).


These are the current CLI options supported, when starting the application:
       
root@ubuntu:/home/fabricio/Documents# python3 eNB_LOCAL.py -h
Usage: eNB_LOCAL.py [options]

Options:
  -h, --help            show this help message and exit
  -i ENB_IP, --ip=ENB_IP
                        eNB Local IP Address
  -m MME_IP, --mme=MME_IP
                        MME IP Address
  -g GATEWAY_IP_ADDRESS, --gateway_ip_address=GATEWAY_IP_ADDRESS
                        gateway IP address
  -u SERIAL_INTERFACE, --usb_device=SERIAL_INTERFACE
                        usb tty (e.g /dev/ttyUSBx)
  -I IMSI, --imsi=IMSI  IMSI (15 digits)
  -E IMEI, --imei=IMEI  IMEI-SV (16 digits)



And this is the application user interface, where we can see the current options and procedures supported:



As refered before, this application also implements the S1-U, and after an sucessful Attach with PDN Connectivy activation, you can use the laptop applications (browser, terminal, etc...) to send/receive traffic over the GTP-U connection towards the SGW using the session IP address, using a tunnel interface.

In case the session is a NB-IoT session you can also send the user plane over NAS.

The application supports currently the following options:
  • S1 Setup type: LTE, NB-IoT, or both
  • Mobile Identity Type: IMSI or GUTI
  • Attach PDN: Default APN, or Specific APN
  • Session Type: 4G, 5G or NB-IoT
  • NB-IoT Session Type: No PSM and eDRX, PSM, eDRX or both PSM and eDRX
  • PDN Type ipv4, ipv6 or ipv4v6
  • Control Plane Service Request with Radio Bearer or without Radio Bearer
  • Attach Type: EPS Attach or Combined EPS/IMSI Attach
  • TAU Type: TA Updating, Combined TA/LA Updating or Combined TA/LA Updating with IMSI Attach
  • Process Paging: Enabled or Disabled
  • SMS Update type: Additional Update Type SMS Only: False or True
  • eNB Cell can change

In terms of procedures, the application supports the following ones:
  • S1 Setup Request
  • S1 Reset
  • Attach
  • Detach
  • TAU
  • TAU Periodic
  • Service Request
  • UE Context Release
  • Send SMS
  • Control Plane Service Request
  • E-RAB Modification Indication
  • Secondary RAT Data Usage Report
  • PDP Connectivity
  • PDN Disconnect
  • Activate/Deactivate GTP-U for Control Plane
  • Activate/Deactivate Data over NAS

A trace of the eNB emulator working can be downloaded here


In the upcoming posts I will talk about other applications I've done to interwork with the MME, mainly the HSS Server, the SGs Server and the SGd Server.

This project is available in my github page https://github.com/fasferraz/eNB

2 comments:

  1. This is awesome. Just to clarify, SIM reader is ordinary ZTE dongle that can return auth. vectors. Brilliant.
    Can you share this on github ?

    ReplyDelete
    Replies
    1. No. You send the RAND and AUTN to the SIM, and it answers back with CK, IK and RES (in case AUTN is valid). With CK and IK and PLMN MNC/MNC you can calculate the KASME and derive the other keys. I will probably share all the code in github.

      Delete