├── .gitignore ├── README.md ├── lib ├── __init__.py ├── log.py ├── network.py ├── queue.py └── radio.py ├── sdr_tmsi_map.py └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GR-GSM based TMSI sniffer 2 | This is just the proof of concept code for MSISDN -> TMSI mapping. 3 | The algorithm is very simple and was already described in the past. 4 | 5 | #### Terms of use 6 | This code is distributed in hope, that it may be helpful for someone, who is learning mobile communications. I am not responsible for possible damage, caused by someone using this software. Please, use it for research and/or education purposes only. 7 | 8 | #### The algorithm 9 | 1. Allocate a new array for further recording; 10 | 2. Start recording, adding every TMSI into the allocated array; 11 | 3. Cause a Paging Request for the victim's phone (call, SMS); 12 | 4. Stop recording; 13 | 5. Repeat above steps, until we get one TMSI, which can be found in all recorded arrays. 14 | 15 | #### Possible difficulties 16 | - TMSI Reallocation. In some networks it happens very rarely, so you can call someone, send an SMS, reboot your phone, and TMSI will be the same as before. Other networks change TMSI after a fixed number of connections. Some networks change TMSI for every dedicated connection, and in this case this toolkit is useless. 17 | - Subscriber notifications. What would you think, if someone call you (or send SMS) multiple times? Something strange and fishily, right? So, instead of calling or sending short messages, it is possible to cause a Paging Request without subscriber notification. One way is to use silent/broken SMS. Another way is to initiate a call to the victim and hangup before the phone will ring. 18 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axilirator/tmsi-sniffer/1177accb9de4fa48049fc9dc53c5988bf40b5a8e/lib/__init__.py -------------------------------------------------------------------------------- /lib/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | 4 | # GR-GSM based TMSI sniffer 5 | # 6 | # Research purposes only 7 | # Use at your own risk 8 | # 9 | # Copyright (C) 2016 Vadim Yanitskiy 10 | # 11 | # All Rights Reserved 12 | # 13 | # This program is free software; you can redistribute it and/or modify 14 | # it under the terms of the GNU Affero General Public License as published by 15 | # the Free Software Foundation; either version 3 of the License, or 16 | # (at your option) any later version. 17 | # 18 | # This program is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | # GNU Affero General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU Affero General Public License 24 | # along with this program. If not, see . 25 | 26 | DAPP = "\033[1;35m" # Application messages 27 | DGSM = "\033[1;33m" # Radio interface messages 28 | DCTL = "\033[1;37m" # Control interface messages 29 | DMII = "\033[1;34m" # TMSI in IDLE mode 30 | DMIR = "\033[1;31m" # TMSI in RECORDING mode 31 | ENDC = '\033[0m' # Default style 32 | 33 | DINFO = "[i] " 34 | DERROR = "[!] " 35 | DPAGING = "[+] " 36 | 37 | def printl(cat, level, msg): 38 | print cat + level + msg + ENDC 39 | -------------------------------------------------------------------------------- /lib/network.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | 4 | # GR-GSM based TMSI sniffer 5 | # Networking tools 6 | # 7 | # Research purposes only 8 | # Use at your own risk 9 | # 10 | # Copyright (C) 2016 Vadim Yanitskiy 11 | # 12 | # All Rights Reserved 13 | # 14 | # This program is free software; you can redistribute it and/or modify 15 | # it under the terms of the GNU Affero General Public License as published by 16 | # the Free Software Foundation; either version 3 of the License, or 17 | # (at your option) any later version. 18 | # 19 | # This program is distributed in the hope that it will be useful, 20 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | # GNU Affero General Public License for more details. 23 | # 24 | # You should have received a copy of the GNU Affero General Public License 25 | # along with this program. If not, see . 26 | 27 | import socket 28 | import select 29 | 30 | from log import * 31 | 32 | class UDPServer: 33 | udp_rx_size = 1024 34 | 35 | def __init__(self, bind_port, remote_addr=False, remote_port=False): 36 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 37 | self.sock.bind(('0.0.0.0', bind_port)) 38 | self.sock.setblocking(0) 39 | 40 | self.udp_bind_port = bind_port 41 | self.udp_remote_addr = remote_addr 42 | self.udp_remote_port = remote_port 43 | 44 | def close(self): 45 | self.sock.close(); 46 | 47 | def handle_rx_event(self): 48 | data, addr = self.sock.recvfrom(self.udp_rx_size) 49 | self.handle_rx_data(data) 50 | 51 | def handle_rx_data(self, data): 52 | raise NotImplementedError 53 | 54 | def send(self, data): 55 | if self.udp_remote_addr == False or self.udp_remote_port == False: 56 | raise Exception("UDP remote addr/port isn't set!") 57 | 58 | self.sock.sendto(data, (self.udp_remote_addr, self.udp_remote_port)) 59 | 60 | class TCPServer: 61 | tcp_rx_size = 1024 62 | connections = [] 63 | max_conn = 10 64 | 65 | def __init__(self): 66 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 67 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 68 | 69 | def listen(self, bind_port, bind_host = "0.0.0.0"): 70 | self.sock.bind((bind_host, bind_port)) 71 | self.sock.listen(self.max_conn) 72 | printl(DCTL, DINFO, "Server started, waiting for connections...") 73 | 74 | def accept(self): 75 | # Attempt to accept a new connection 76 | sockfd, addr = self.sock.accept() 77 | 78 | # Register this connection 79 | self.connections.append(sockfd) 80 | printl(DCTL, DINFO, "New connection from %s:%s" % addr) 81 | 82 | def handle_rx_event(self, socks): 83 | # Find all ready to read sockets 84 | for sock in self.connections: 85 | # Ok, we found one 86 | if sock in socks: 87 | data = sock.recv(self.tcp_rx_size) 88 | 89 | # Detect connection close 90 | if len(data) == 0: 91 | self.connections.remove(sock) 92 | self.handle_close_event() 93 | else: 94 | self.handle_rx_data(data) 95 | 96 | def send(self, sock, data): 97 | try: 98 | sock.send(data) 99 | except: 100 | sock.close() 101 | self.connections.remove(sock) 102 | self.handle_close_event() 103 | 104 | def broadcast(self, data): 105 | for sock in self.connections: 106 | self.send(sock, data) 107 | 108 | def handle_rx_data(self, data): 109 | raise NotImplementedError 110 | 111 | def handle_close_event(self): 112 | raise NotImplementedError 113 | 114 | def close(self): 115 | self.sock.close(); 116 | 117 | class TCPClient: 118 | tcp_rx_size = 1024 119 | 120 | def __init__(self): 121 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 122 | 123 | def connect(self, remote_addr, remote_port): 124 | printl(DCTL, DINFO, "Connecting to %s:%d..." 125 | % (remote_addr, remote_port)) 126 | try: 127 | self.sock.connect((remote_addr, remote_port)) 128 | return True 129 | except: 130 | printl(DCTL, DERROR, "Connection refused!") 131 | return False 132 | 133 | def close(self): 134 | self.sock.close(); 135 | 136 | def handle_rx_event(self): 137 | data = self.sock.recv(self.tcp_rx_size) 138 | 139 | # Detect connection close 140 | if len(data) == 0: 141 | self.handle_close_event() 142 | else: 143 | self.handle_rx_data(data) 144 | 145 | def handle_rx_data(self, data): 146 | raise NotImplementedError 147 | 148 | def handle_close_event(self): 149 | raise NotImplementedError 150 | 151 | def send(self, data): 152 | self.sock.send(data) 153 | -------------------------------------------------------------------------------- /lib/queue.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | 4 | # GR-GSM based TMSI sniffer 5 | # 6 | # Research purposes only 7 | # Use at your own risk 8 | # 9 | # Copyright (C) 2016 Vadim Yanitskiy 10 | # 11 | # All Rights Reserved 12 | # 13 | # This program is free software; you can redistribute it and/or modify 14 | # it under the terms of the GNU Affero General Public License as published by 15 | # the Free Software Foundation; either version 3 of the License, or 16 | # (at your option) any later version. 17 | # 18 | # This program is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | # GNU Affero General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU Affero General Public License 24 | # along with this program. If not, see . 25 | 26 | class Queue: 27 | def __init__(self, a = False, b = False): 28 | self.items = [] 29 | 30 | if a != False and b != False: 31 | for item in a.items: 32 | if item in b.items: 33 | self.add(item) 34 | 35 | def add(self, item): 36 | self.items.append(item) 37 | 38 | def find(self, item): 39 | return item in self.items 40 | 41 | def unique(self): 42 | unique_items = [] 43 | 44 | for item in self.items: 45 | if not item in unique_items: 46 | unique_items.append(item) 47 | 48 | self.items = unique_items 49 | 50 | def remove(self, item): 51 | while item in self.items: 52 | self.items.remove(item) 53 | -------------------------------------------------------------------------------- /lib/radio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | 4 | # GR-GSM based TMSI sniffer 5 | # 6 | # Research purposes only 7 | # Use at your own risk 8 | # 9 | # Copyright (C) 2016 Vadim Yanitskiy 10 | # 11 | # All Rights Reserved 12 | # 13 | # This program is free software; you can redistribute it and/or modify 14 | # it under the terms of the GNU Affero General Public License as published by 15 | # the Free Software Foundation; either version 3 of the License, or 16 | # (at your option) any later version. 17 | # 18 | # This program is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | # GNU Affero General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU Affero General Public License 24 | # along with this program. If not, see . 25 | 26 | import pmt 27 | import time 28 | import grgsm 29 | import osmosdr 30 | 31 | from math import pi 32 | from log import * 33 | 34 | from gnuradio.eng_option import eng_option 35 | from gnuradio import eng_notation 36 | from gnuradio.filter import firdes 37 | from gnuradio import blocks 38 | from gnuradio import gr 39 | 40 | class RadioInterface(gr.top_block): 41 | fc = 935e6 # ARFCN 0 (initial setup) 42 | shiftoff = 400e3 43 | 44 | def __init__(self, phy_device_args, phy_subdev_spec, 45 | phy_sample_rate, phy_gain, phy_ppm, sock_port): 46 | printl(DGSM, DINFO, "Init Radio interface") 47 | 48 | self.device_args = phy_device_args 49 | self.subdev_spec = phy_subdev_spec 50 | self.samp_rate = phy_sample_rate 51 | self.gain = phy_gain 52 | self.ppm = phy_ppm 53 | 54 | gr.top_block.__init__(self, "GR-GSM TMSI sniffer") 55 | shift_fc = self.fc - self.shiftoff 56 | 57 | ################################################## 58 | # PHY Definition 59 | ################################################## 60 | self.phy = osmosdr.source( 61 | args = "numchan=%d %s" % (1, self.device_args)) 62 | 63 | self.phy.set_bandwidth(250e3 + abs(self.shiftoff), 0) 64 | self.phy.set_center_freq(shift_fc, 0) 65 | self.phy.set_sample_rate(self.samp_rate) 66 | self.phy.set_freq_corr(self.ppm, 0) 67 | self.phy.set_iq_balance_mode(2, 0) 68 | self.phy.set_dc_offset_mode(2, 0) 69 | self.phy.set_gain_mode(False, 0) 70 | self.phy.set_gain(self.gain, 0) 71 | self.phy.set_if_gain(20, 0) 72 | self.phy.set_bb_gain(20, 0) 73 | self.phy.set_antenna("", 0) 74 | 75 | ################################################## 76 | # GR-GSM Magic 77 | ################################################## 78 | self.gsm_bcch_ccch_demapper = grgsm.gsm_bcch_ccch_demapper( 79 | timeslot_nr = 0) 80 | 81 | self.blocks_rotator = blocks.rotator_cc( 82 | -2 * pi * self.shiftoff / self.samp_rate) 83 | 84 | self.gsm_input = grgsm.gsm_input( 85 | ppm = self.ppm, osr = 4, fc = self.fc, 86 | samp_rate_in = self.samp_rate) 87 | 88 | self.socket_pdu = blocks.socket_pdu( 89 | "UDP_CLIENT", "127.0.0.1", str(sock_port), 10000, False) 90 | 91 | self.gsm_clck_ctrl = grgsm.clock_offset_control( 92 | shift_fc, self.samp_rate, osr = 4) 93 | 94 | self.gsm_ccch_decoder = grgsm.control_channels_decoder() 95 | self.gsm_receiver = grgsm.receiver(4, ([0]), ([])) 96 | 97 | ################################################## 98 | # Connections 99 | ################################################## 100 | self.connect((self.phy, 0), (self.blocks_rotator, 0)) 101 | self.connect((self.blocks_rotator, 0), (self.gsm_input, 0)) 102 | self.connect((self.gsm_input, 0), (self.gsm_receiver, 0)) 103 | 104 | self.msg_connect((self.gsm_receiver, 'C0'), 105 | (self.gsm_bcch_ccch_demapper, 'bursts')) 106 | 107 | self.msg_connect((self.gsm_receiver, 'measurements'), 108 | (self.gsm_clck_ctrl, 'measurements')) 109 | 110 | self.msg_connect((self.gsm_clck_ctrl, 'ctrl'), 111 | (self.gsm_input, 'ctrl_in')) 112 | 113 | self.msg_connect((self.gsm_bcch_ccch_demapper, 'bursts'), 114 | (self.gsm_ccch_decoder, 'bursts')) 115 | 116 | self.msg_connect((self.gsm_ccch_decoder, 'msgs'), 117 | (self.socket_pdu, 'pdus')) 118 | 119 | def shutdown(self): 120 | printl(DGSM, DINFO, "Shutdown Radio interface") 121 | self.stop() 122 | self.wait() 123 | 124 | def get_args(self): 125 | return self.args 126 | 127 | def set_args(self, args): 128 | self.args = args 129 | 130 | def get_fc(self): 131 | return self.fc 132 | 133 | def set_fc(self, fc): 134 | self.phy.set_center_freq(fc - self.shiftoff, 0) 135 | self.gsm_input.set_fc(fc) 136 | self.fc_set = True 137 | self.fc = fc 138 | 139 | def get_gain(self): 140 | return self.gain 141 | 142 | def set_gain(self, gain): 143 | self.phy.set_gain(gain, 0) 144 | self.gain = gain 145 | 146 | def get_ppm(self): 147 | return self.ppm 148 | 149 | def set_ppm(self, ppm): 150 | self.rtlsdr_source_0.set_freq_corr(ppm, 0) 151 | self.ppm = ppm 152 | 153 | def get_samp_rate(self): 154 | return self.samp_rate 155 | 156 | def set_samp_rate(self, samp_rate): 157 | self.blocks_rotator.set_phase_inc( 158 | -2 * pi * self.shiftoff / samp_rate) 159 | 160 | self.gsm_input.set_samp_rate_in(samp_rate) 161 | self.phy.set_sample_rate(samp_rate) 162 | self.samp_rate = samp_rate 163 | 164 | def get_shiftoff(self): 165 | return self.shiftoff 166 | 167 | def set_shiftoff(self, shiftoff): 168 | self.blocks_rotator.set_phase_inc( 169 | -2 * pi * shiftoff / self.samp_rate) 170 | 171 | self.phy.set_bandwidth(250e3 + abs(shiftoff), 0) 172 | self.phy.set_center_freq(self.fc - shiftoff, 0) 173 | self.shiftoff = shiftoff 174 | -------------------------------------------------------------------------------- /sdr_tmsi_map.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | 4 | # GR-GSM based TMSI sniffer 5 | # 6 | # Research purposes only 7 | # Use at your own risk 8 | # 9 | # Copyright (C) 2016 Vadim Yanitskiy 10 | # 11 | # All Rights Reserved 12 | # 13 | # This program is free software; you can redistribute it and/or modify 14 | # it under the terms of the GNU Affero General Public License as published by 15 | # the Free Software Foundation; either version 3 of the License, or 16 | # (at your option) any later version. 17 | # 18 | # This program is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | # GNU Affero General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU Affero General Public License 24 | # along with this program. If not, see . 25 | 26 | import sys 27 | import getopt 28 | import signal 29 | 30 | from lib.network import * 31 | from lib.radio import * 32 | from lib.queue import * 33 | from lib.log import * 34 | 35 | class TMSIManager(UDPServer): 36 | def __init__(self, local_port): 37 | printl(DMII, DINFO, "Init TMSI Manager") 38 | UDPServer.__init__(self, local_port) 39 | self.flush() 40 | 41 | def shutdown(self): 42 | printl(DMII, DINFO, "Shutdown TMSI Manager") 43 | self.close() 44 | 45 | def handle_rx_data(self, data): 46 | # Convert to a byte array 47 | data = bytearray(data) 48 | # Cut GSMTAP header 49 | l3 = data[16:] 50 | 51 | # We need Paging Requests only 52 | if l3[1] == 0x06: 53 | if l3[2] == 0x21: 54 | self.handle_p1(l3) 55 | elif l3[2] == 0x22: 56 | self.handle_p2(l3) 57 | elif l3[2] == 0x24: 58 | self.handle_p3(l3) 59 | 60 | def print_tmsi(self, tmsi): 61 | msg = "Paging Request to 0x" 62 | for x in tmsi: 63 | msg += "{:02x}".format(x) 64 | 65 | cat = DMIR if self.recording else DMII 66 | printl(cat, DPAGING, msg) 67 | 68 | def handle_tmsi(self, tmsi): 69 | # Yes, print this one first 70 | self.print_tmsi(tmsi) 71 | 72 | # Determine our current state 73 | if self.recording: 74 | # Add every possible TMSI 75 | self.record.add(tmsi) 76 | else: 77 | # Filter outsider TMSIs 78 | for record in self.records: 79 | record.remove(tmsi) 80 | 81 | def handle_p1(self, l3): 82 | # This can contain two MIs 83 | mi_type = l3[5] & 0x07 84 | mi_found = False 85 | msg_len = l3[0] 86 | 87 | # FIXME: What about IMEI and IMEISV??? 88 | if mi_type == 0x04: # TMSI 89 | self.handle_tmsi(l3[6:10]) 90 | next_mi_index = 10 91 | mi_found = True 92 | elif mi_type == 0x01: # IMSI 93 | next_mi_index = 13 94 | mi_found = True 95 | 96 | # Check if there is an additional MI 97 | if mi_found: 98 | if next_mi_index < (msg_len + 1) and l3[next_mi_index] == 0x17: 99 | # Extract MI type and length 100 | mi_type = l3[next_mi_index + 2] & 0x07 101 | mi2_len = l3[next_mi_index + 1] 102 | 103 | # We only need TMSI 104 | if mi_type == 0x04: 105 | a = next_mi_index + 3 106 | b = next_mi_index + 7 107 | self.handle_tmsi(l3[a:b]) 108 | 109 | def handle_p2(self, l3): 110 | # This can contain two TMSIs and (optionally) one more MI 111 | self.handle_tmsi(l3[4:8]) 112 | self.handle_tmsi(l3[8:12]) 113 | 114 | # Check for optional TMSI 115 | if l3[14] & 0x07 == 0x04: 116 | self.handle_tmsi(l3[15:19]) 117 | 118 | def handle_p3(self, l3): 119 | # This one contains four TMSIs 120 | self.handle_tmsi(l3[4:8]) 121 | self.handle_tmsi(l3[8:12]) 122 | self.handle_tmsi(l3[12:16]) 123 | self.handle_tmsi(l3[16:20]) 124 | 125 | def start(self): 126 | self.record = Queue() 127 | self.recording = True 128 | 129 | def stop(self): 130 | self.record.unique() # We don't need any duplicates 131 | self.records.append(self.record) 132 | self.recording = False 133 | 134 | def flush(self): 135 | self.records = [] 136 | self.record = Queue() 137 | self.recording = False 138 | 139 | def cross(self): 140 | if len(self.records) > 1: 141 | result = self.records[0] 142 | 143 | for recod in self.records[1:]: 144 | result = Queue(result, recod) 145 | 146 | return result 147 | else: 148 | return [] 149 | 150 | class ControlInterface(TCPClient): 151 | def __init__(self, app): 152 | printl(DCTL, DINFO, "Init Control interface") 153 | TCPClient.__init__(self) 154 | self.app = app 155 | 156 | def shutdown(self): 157 | printl(DCTL, DINFO, "Shutdown Control interface") 158 | self.close() 159 | 160 | def verify_req(self, data): 161 | # Verify command signature 162 | return data.startswith("CMD") 163 | 164 | def prepare_req(self, data): 165 | # Strip signature, paddings and \0 166 | request = data[4:].strip().strip("\0") 167 | # Split into a command and arguments 168 | request = request.split(" ") 169 | # Now we have something like ["RXTUNE", "941600"] 170 | return request 171 | 172 | def verify_cmd(self, request, cmd, argc): 173 | # If requested command matches and has enough arguments 174 | if request[0] == cmd and len(request) - 1 == argc: 175 | # Check if all arguments are numeric 176 | for v in request[1:]: 177 | if not v.isdigit(): 178 | return False 179 | 180 | # Ok, everything is fine 181 | return True 182 | else: 183 | return False 184 | 185 | def parse_cmd(self, request): 186 | if self.verify_cmd(request, "RXTUNE", 1): 187 | printl(DCTL, DINFO, "Recv RXTUNE cmd") 188 | freq = int(request[1]) * 1000 189 | 190 | printl(DCTL, DINFO, "Switching to %d Hz" % freq) 191 | self.app.radio.set_fc(freq) 192 | 193 | elif self.verify_cmd(request, "START", 0): 194 | printl(DCTL, DINFO, "Recv START cmd") 195 | self.app.tmsi_mgr.start() 196 | 197 | elif self.verify_cmd(request, "STOP", 0): 198 | printl(DCTL, DINFO, "Recv STOP cmd") 199 | self.app.tmsi_mgr.stop() 200 | 201 | elif self.verify_cmd(request, "CROSS", 0): 202 | printl(DCTL, DINFO, "Recv CROSS cmd") 203 | 204 | result = self.app.tmsi_mgr.cross() 205 | response = "CROSS Result:\n" 206 | for tmsi in result.items: 207 | response += "0x" 208 | for x in tmsi: 209 | response += "{:02x}".format(x) 210 | response += "\n" 211 | 212 | self.send(response) 213 | 214 | elif self.verify_cmd(request, "FLUSH", 0): 215 | printl(DCTL, DINFO, "Recv FLUSH cmd") 216 | self.app.tmsi_mgr.flush() 217 | 218 | # Wrong command 219 | else: 220 | printl(DCTL, DERROR, "Wrong command on CTRL interface") 221 | 222 | def handle_rx_data(self, data): 223 | if self.verify_req(data): 224 | request = self.prepare_req(data) 225 | self.parse_cmd(request) 226 | else: 227 | printl(DCTL, DERROR, "Wrong data on CTRL interface") 228 | 229 | def handle_close_event(self): 230 | printl(DCTL, DERROR, "Disconnected from server") 231 | self.app.quit = True 232 | 233 | class Application: 234 | # Application variables 235 | master_addr = "127.0.0.1" 236 | master_port = 8888 237 | local_port = 4729 238 | quit = False 239 | 240 | # PHY specific variables 241 | phy_sample_rate = 2000000 242 | phy_subdev_spec = "" 243 | phy_device_args = "" 244 | phy_gain = 30 245 | phy_ppm = 0 246 | 247 | def __init__(self): 248 | self.print_copyright() 249 | self.parse_argv() 250 | 251 | # Set up signal handlers 252 | signal.signal(signal.SIGINT, self.sig_handler) 253 | 254 | def run(self): 255 | # Init Control interface 256 | self.ctrl = ControlInterface(self) 257 | if not self.ctrl.connect(self.master_addr, self.master_port): 258 | sys.exit(1) 259 | 260 | # Init Radio interface 261 | self.radio = RadioInterface( 262 | self.phy_device_args, self.phy_subdev_spec, 263 | self.phy_sample_rate, self.phy_gain, 264 | self.phy_ppm, self.local_port) 265 | self.radio.start() 266 | 267 | # Init TMSI manager 268 | self.tmsi_mgr = TMSIManager(self.local_port) 269 | 270 | # Enter main loop 271 | printl(DAPP, DINFO, "Init complete, entering main loop...") 272 | while True: 273 | # Check if it's time to quit 274 | if self.quit: 275 | self.shutdown() 276 | break 277 | 278 | # Keep working 279 | self.loop() 280 | 281 | def loop(self): 282 | # Blocking select 283 | r_event, w_event, x_event = select.select( 284 | [self.ctrl.sock, self.tmsi_mgr.sock], [], []) 285 | 286 | # Check for incoming GSM data 287 | if self.tmsi_mgr.sock in r_event: 288 | self.tmsi_mgr.handle_rx_event() 289 | 290 | # Check for incoming CTRL commands 291 | if self.ctrl.sock in r_event: 292 | self.ctrl.handle_rx_event() 293 | 294 | def shutdown(self): 295 | printl(DAPP, DINFO, "Shutting down...") 296 | self.ctrl.shutdown() 297 | self.radio.shutdown() 298 | self.tmsi_mgr.shutdown() 299 | 300 | def print_copyright(self): 301 | s = "Copyright (C) 2016 by Vadim Yanitskiy \n" \ 302 | "License GPLv2+: GNU GPL version 2 or later \n" \ 303 | "This is free software: you are free to change and redistribute it.\n" \ 304 | "There is NO WARRANTY, to the extent permitted by law.\n" 305 | 306 | print s 307 | 308 | def print_help(self): 309 | s = " Usage: " + sys.argv[0] + " [options]\n\n" \ 310 | " Some help...\n" \ 311 | " -h --help this text\n" \ 312 | " -w --write Write logs into a file\n\n" 313 | 314 | # TRX specific 315 | s += " Master server specific\n" \ 316 | " -i --master-addr Set IP address of master server\n" \ 317 | " -p --master-port Set port number (default 8888)\n\n" 318 | 319 | # PHY specific 320 | s += " Radio interface specific\n" \ 321 | " -l --local-port Change a local port (default 8899)\n" \ 322 | " -a --device-args Set device arguments\n" \ 323 | " -s --sample-rate Set PHY sample rate (default 2000000)\n" \ 324 | " -S --subdev-spec Set PHY sub-device specification\n" \ 325 | " -g --gain Set PHY gain (default 30)\n" \ 326 | " --ppm Set PHY frequency correction (default 0)\n" 327 | 328 | print s 329 | 330 | def parse_argv(self): 331 | try: 332 | opts, args = getopt.getopt(sys.argv[1:], 333 | "w:i:p:l:a:s:S:g:h", 334 | ["help", "arfcn=", "gain=", "ppm=", "write=", 335 | "master-addr=", "master-port=", "local-port=", 336 | "device-args=", "sample-rate=", "subdev-spec="]) 337 | except getopt.GetoptError as err: 338 | # Print help and exit 339 | self.print_help() 340 | print "[!] " + str(err) 341 | sys.exit(2) 342 | 343 | for o, v in opts: 344 | if o in ("-h", "--help"): 345 | self.print_help() 346 | sys.exit(2) 347 | elif o in ("-w", "--write"): 348 | self.master_addr = v 349 | 350 | # Master interface specific 351 | elif o in ("-i", "--master-addr"): 352 | self.master_addr = v 353 | elif o in ("-p", "--master-port"): 354 | if int(v) >= 0 and int(v) <= 65535: 355 | self.master_port = int(v) 356 | else: 357 | print "[!] Port number should be in range [0-65536]" 358 | sys.exit(2) 359 | 360 | # PHY specific 361 | elif o in ("-a", "--device-args"): 362 | self.phy_device_args = v 363 | elif o in ("-g", "--gain"): 364 | self.phy_gain = int(v) 365 | elif o in ("-S", "--subdev-spec"): 366 | self.phy_subdev_spec = v 367 | elif o in ("-s", "--sample-rate"): 368 | self.phy_sample_rate = int(v) 369 | elif o in ("--ppm"): 370 | self.phy_ppm = int(v) 371 | elif o in ("-l", "--local-port"): 372 | if int(v) >= 0 and int(v) <= 65535: 373 | self.local_port = int(v) 374 | else: 375 | print "[!] Port number should be in range [0-65536]" 376 | sys.exit(2) 377 | 378 | def sig_handler(self, signum, frame): 379 | print "Signal %d received" % signum 380 | if signum is signal.SIGINT: 381 | self.shutdown() 382 | sys.exit(0) 383 | 384 | if __name__ == '__main__': 385 | Application().run() 386 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | 4 | # GR-GSM based TMSI sniffer 5 | # 6 | # Research purposes only 7 | # Use at your own risk 8 | # 9 | # Copyright (C) 2016 Vadim Yanitskiy 10 | # 11 | # All Rights Reserved 12 | # 13 | # This program is free software; you can redistribute it and/or modify 14 | # it under the terms of the GNU Affero General Public License as published by 15 | # the Free Software Foundation; either version 3 of the License, or 16 | # (at your option) any later version. 17 | # 18 | # This program is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | # GNU Affero General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU Affero General Public License 24 | # along with this program. If not, see . 25 | 26 | import os 27 | import sys 28 | import getopt 29 | import signal 30 | 31 | from lib.network import * 32 | from lib.log import * 33 | 34 | class CommandLine: 35 | def handle_rx_event(self): 36 | cmd = sys.stdin.readline() 37 | self.handle_cmd(cmd) 38 | 39 | def write(self, data): 40 | sys.stdout.write(data) 41 | sys.stdout.flush() 42 | 43 | def print_help(self): 44 | self.write("\n") 45 | self.write(" Control interface help\n") 46 | self.write(" ======================\n\n") 47 | self.write(" help this text\n") 48 | self.write(" clear clear screen\n") 49 | self.write(" exit shutdown server\n") 50 | self.write("\n") 51 | self.write(" rxtune tunes slaves to a given frequency in kHz\n") 52 | self.write(" paging TMSI mapping\n") 53 | self.write(" | start start recording\n") 54 | self.write(" | stop stop recording\n") 55 | self.write(" | cross show results\n") 56 | self.write(" | flush reset all recordings\n") 57 | self.write("\n") 58 | 59 | def print_unknown(self): 60 | self.write("Unknown command, see help.\n") 61 | 62 | def print_prompt(self): 63 | self.write("CTRL# ") 64 | 65 | def handle_cmd(self, request): 66 | # Strip spaces and \0 67 | request = request.strip().strip("\0") 68 | # Split into a command and arguments 69 | request = request.split(" ") 70 | argv = request[1:] 71 | argc = len(argv) 72 | cmd = request[0] 73 | 74 | # Application specific 75 | if cmd == "exit": 76 | app.shutdown() 77 | elif cmd == "help": 78 | self.print_help() 79 | elif cmd == "clear": 80 | os.system("clear") 81 | 82 | # Server specific 83 | elif cmd == "rxtune" and argc == 1: 84 | app.server.broadcast("CMD RXTUNE %s\n" % argv[0]) 85 | elif cmd == "paging": 86 | if argc == 1: 87 | subcmd = argv[0] 88 | if subcmd == "start": 89 | app.server.broadcast("CMD START\n") 90 | elif subcmd == "stop": 91 | app.server.broadcast("CMD STOP\n") 92 | elif subcmd == "cross": 93 | app.server.broadcast("CMD CROSS\n") 94 | elif subcmd == "flush": 95 | app.server.broadcast("CMD FLUSH\n") 96 | 97 | # Unknown command 98 | elif cmd != "": 99 | self.print_unknown() 100 | 101 | class Server(TCPServer): 102 | def __init__(self): 103 | TCPServer.__init__(self) 104 | 105 | def handle_close_event(self): 106 | printl(DCTL, DINFO, "A slave node disconnected") 107 | 108 | def handle_rx_data(self, data): 109 | printl(DCTL, DINFO, "Some slave says:") 110 | app.ctrl.write(data) 111 | 112 | class Application: 113 | # Application variables 114 | listen_port = 8888 115 | 116 | def __init__(self): 117 | self.print_copyright() 118 | self.parse_argv() 119 | 120 | # Set up signal handlers 121 | signal.signal(signal.SIGINT, self.sig_handler) 122 | 123 | def run(self): 124 | self.ctrl = CommandLine() 125 | self.server = Server() 126 | self.server.listen(self.listen_port) 127 | 128 | printl(DAPP, DINFO, "Init complete") 129 | self.ctrl.print_help() 130 | 131 | # Enter main loop 132 | while True: 133 | # Keep working 134 | self.loop() 135 | 136 | def loop(self): 137 | # Provide prompt 138 | # TODO: How to save a previous command? 139 | self.ctrl.print_prompt() 140 | 141 | # Blocking select 142 | r_list = [sys.stdin, self.server.sock] + self.server.connections 143 | r_event, w_event, x_event = select.select(r_list, [], []) 144 | 145 | # Check for incoming CTRL commands 146 | if sys.stdin in r_event: 147 | self.ctrl.handle_rx_event() 148 | 149 | # Check for incoming data 150 | elif self.server.sock in r_event: 151 | self.server.accept() 152 | 153 | # Maybe something from slaves? 154 | else: 155 | self.server.handle_rx_event(r_event) 156 | 157 | def shutdown(self): 158 | printl(DAPP, DINFO, "Shutting down...") 159 | self.server.close() 160 | sys.exit(0) 161 | 162 | def print_copyright(self): 163 | s = "Copyright (C) 2016 by Vadim Yanitskiy \n" \ 164 | "License GPLv2+: GNU GPL version 2 or later \n" \ 165 | "This is free software: you are free to change and redistribute it.\n" \ 166 | "There is NO WARRANTY, to the extent permitted by law.\n" 167 | 168 | print s 169 | 170 | def print_help(self): 171 | s = " Usage: " + sys.argv[0] + " [options]\n\n" \ 172 | " Some help...\n" \ 173 | " -h --help this text\n" \ 174 | " -p --port Specify a port to listen (default 8888)\n" 175 | 176 | print s 177 | 178 | def parse_argv(self): 179 | try: 180 | opts, args = getopt.getopt(sys.argv[1:], 181 | "p:h", ["help", "port="]) 182 | except getopt.GetoptError as err: 183 | # Print help and exit 184 | self.print_help() 185 | print "[!] " + str(err) 186 | sys.exit(2) 187 | 188 | for o, v in opts: 189 | if o in ("-h", "--help"): 190 | self.print_help() 191 | sys.exit(2) 192 | elif o in ("-p", "--port"): 193 | if int(v) >= 0 and int(v) <= 65535: 194 | self.listen_port = int(v) 195 | else: 196 | print "[!] Port number should be in range [0-65536]" 197 | sys.exit(2) 198 | 199 | def sig_handler(self, signum, frame): 200 | print "Signal %d received" % signum 201 | if signum is signal.SIGINT: 202 | self.shutdown() 203 | 204 | if __name__ == '__main__': 205 | app = Application() 206 | app.run() 207 | --------------------------------------------------------------------------------