├── icc ├── __init__.py ├── detectors │ ├── __init__.py │ ├── id_request_detector.py │ ├── cell_reselection_hysteresis.py │ ├── cell_reselection_offset.py │ ├── a5_detector.py │ ├── tic.py │ └── detector.py ├── aux │ ├── __init__.py │ ├── TowerRank.py │ ├── lat_log_utils.py │ └── ChannelInfo.py ├── models │ ├── __init__.py │ ├── Scan.py │ ├── CellObservation.py │ ├── CellTowerScan.py │ └── UUID.py ├── cellinfochecks │ ├── __init__.py │ ├── query_cell_tower.py │ ├── lac.py │ ├── tower.py │ ├── tic.py │ └── neighbours.py ├── database │ └── __init__.py ├── detector_manager.py ├── gsmpackets.py ├── file_analyzer.py ├── analyzer.py ├── runner.py └── scanner.py ├── report ├── elsarticle-template.spl ├── img │ ├── icc.png │ └── scanner.png ├── elsarticle-template.pdf ├── elsarticle-template.synctex.gz ├── pdflatex7858.fls ├── elsarticle-template.bbl ├── elsarticle-template.aux ├── sample.bib ├── elsarticle-template.blg ├── numcompress.sty ├── elsarticle-template.tex ├── elsarticle-template.fls ├── elsarticle-template.fdb_latexmk ├── elsarticle.cls └── elsarticle-template.log ├── .gitignore ├── SDR_INSTALLATION.md ├── main.py └── README.md /icc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icc/detectors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /report/elsarticle-template.spl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /report/img/icc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santiag0aragon/icc/HEAD/report/img/icc.png -------------------------------------------------------------------------------- /icc/aux/__init__.py: -------------------------------------------------------------------------------- 1 | from ChannelInfo import ChannelInfo 2 | from TowerRank import TowerRank 3 | -------------------------------------------------------------------------------- /report/img/scanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santiag0aragon/icc/HEAD/report/img/scanner.png -------------------------------------------------------------------------------- /report/elsarticle-template.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santiag0aragon/icc/HEAD/report/elsarticle-template.pdf -------------------------------------------------------------------------------- /report/elsarticle-template.synctex.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santiag0aragon/icc/HEAD/report/elsarticle-template.synctex.gz -------------------------------------------------------------------------------- /icc/models/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['CellObservation', 'CellTowerScan', 'Scan'] 2 | 3 | from CellTowerScan import CellTowerScan 4 | from CellObservation import CellObservation 5 | from Scan import Scan 6 | -------------------------------------------------------------------------------- /icc/cellinfochecks/__init__.py: -------------------------------------------------------------------------------- 1 | from neighbours import neighbours 2 | from query_cell_tower import queryTower 3 | from tower import Tower 4 | from tic import tic 5 | from lac import lac 6 | from icc.aux import TowerRank 7 | -------------------------------------------------------------------------------- /report/pdflatex7858.fls: -------------------------------------------------------------------------------- 1 | PWD /Users/santiagoar/Google Drive/Security and Privacy/UTwente/PET/Assignments/a2/report 2 | INPUT /usr/local/texlive/2014/texmf.cnf 3 | INPUT /usr/local/texlive/2014/texmf-dist/web2c/texmf.cnf 4 | -------------------------------------------------------------------------------- /report/elsarticle-template.bbl: -------------------------------------------------------------------------------- 1 | \begin{thebibliography}{} 2 | \expandafter\ifx\csname url\endcsname\relax 3 | \def\url#1{\texttt{#1}}\fi 4 | \expandafter\ifx\csname urlprefix\endcsname\relax\def\urlprefix{URL }\fi 5 | \expandafter\ifx\csname href\endcsname\relax 6 | \def\href#1#2{#2} \def\path#1{#1}\fi 7 | 8 | \end{thebibliography} 9 | -------------------------------------------------------------------------------- /icc/aux/TowerRank.py: -------------------------------------------------------------------------------- 1 | class TowerRank: 2 | def __init__(self, rank, detector, comment, cellobs_id): 3 | self.s_rank = rank 4 | self.detector = detector 5 | self.comment = comment 6 | self.cellobs_id = cellobs_id 7 | 8 | def __repr__(self): 9 | return self.detector + ": " + self.comment + "(" + str(self.s_rank) + ")" 10 | -------------------------------------------------------------------------------- /icc/database/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy import event 5 | from sqlalchemy.orm.exc import NoResultFound,MultipleResultsFound 6 | 7 | engine = create_engine("sqlite:///test_database.sqlite" ) 8 | session_class = sessionmaker(bind=engine) 9 | 10 | Base = declarative_base() 11 | -------------------------------------------------------------------------------- /report/elsarticle-template.aux: -------------------------------------------------------------------------------- 1 | \relax 2 | \@writefile{toc}{\contentsline {section}{\numberline {1}Motivation and Outline}{1}} 3 | \bibstyle{elsarticle-num} 4 | \bibdata{sample} 5 | \@writefile{toc}{\contentsline {section}{\numberline {2}Design}{2}} 6 | \@writefile{toc}{\contentsline {subsection}{\numberline {2.1}Detection methods}{2}} 7 | \@writefile{toc}{\contentsline {section}{\numberline {3}Implementation details}{2}} 8 | \@writefile{toc}{\contentsline {section}{\numberline {4}Limitations and future work}{2}} 9 | -------------------------------------------------------------------------------- /icc/cellinfochecks/query_cell_tower.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from tower import Tower 5 | 6 | def queryTower(mcc, mnc, lac, cell_id): 7 | """ 8 | Queries the database for cell towers selected by the given arguments 9 | Returns the results as a list 10 | """ 11 | 12 | engine = create_engine('sqlite:///opencellid-nl.sqlite') 13 | Session = sessionmaker(bind=engine) 14 | session = Session() 15 | 16 | result_list = session.query(Tower).filter_by(mcc=mcc, net=mnc, area=lac, cell=cell_id).all() 17 | 18 | return result_list 19 | -------------------------------------------------------------------------------- /icc/models/Scan.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from functools import partial 3 | from icc.database import Base 4 | 5 | from UUID import id_column 6 | 7 | # Prevents setting Not Null on each NotNullColumn 8 | NotNullColumn = partial(Column, nullable=False) 9 | 10 | 11 | class Scan(Base): 12 | __tablename__ = 'scans' 13 | id = id_column() 14 | timestamp = NotNullColumn(DateTime(True)) 15 | bands = NotNullColumn(String) 16 | latitude = Column(Float) 17 | longitude = Column(Float) 18 | 19 | def getScanCaptureFileName(self): 20 | return "scan_{}_{}.file".format(self.id, str(self.timestamp)) 21 | -------------------------------------------------------------------------------- /icc/aux/lat_log_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | def dms2dd(degrees, minutes, seconds, direction): 5 | dd = float(degrees) + float(minutes)/60 + float(seconds)/(60*60); 6 | if direction == 'S' or direction == 'W': 7 | dd *= -1 8 | return dd; 9 | 10 | def dd2dms(deg): 11 | d = int(deg) 12 | md = abs(deg - d) * 60 13 | m = int(md) 14 | sd = (md - m) * 60 15 | return [d, m, sd] 16 | 17 | def parse_dms(dms): 18 | parts = re.split('[^\d\w]+', dms) 19 | lat = dms2dd(parts[0], parts[1], parts[2], parts[3]) 20 | lng = dms2dd(parts[4], parts[5], parts[6], parts[7]) 21 | 22 | return (lat, lng) 23 | -------------------------------------------------------------------------------- /icc/detectors/id_request_detector.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | from icc.gsmpackets import * 3 | from detector import Detector 4 | 5 | 6 | class IDRequestDetector(Detector): 7 | 8 | def handle_packet(self, data): 9 | p = GSMTap(data) 10 | if p.payload.name is 'LAPDm': 11 | if p.payload.payload.name is 'GSMAIFDTAP': 12 | if p.payload.payload.payload.name is 'IdentityRequest': 13 | id_type = p.payload.payload.payload.id_type 14 | if id_type == 1: 15 | print 'IMSI request detected' 16 | self.counter += 1 17 | self.update_rank(Detector.UNKNOWN, 'IMSI request detected %s times' % self.counter) 18 | 19 | -------------------------------------------------------------------------------- /icc/cellinfochecks/lac.py: -------------------------------------------------------------------------------- 1 | from icc.aux import TowerRank 2 | from collections import Counter 3 | 4 | lac_threshold = .25 # % of most common LAC 5 | 6 | def lac(found_list): 7 | ranks = [] 8 | for info in sorted(found_list): 9 | rank = 0 10 | comment = "Common local area code" 11 | 12 | ## checking local area code consistency 13 | lacodes = [] 14 | for tower in found_list: 15 | if info.mcc == tower.mcc and info.mnc == info.mnc: 16 | lacodes.append(tower.lac) 17 | 18 | areacounter = Counter(lacodes) 19 | if (areacounter.most_common(1)[0][1] * lac_threshold) > areacounter[info.lac]: 20 | comment = "Uncommon local area code" 21 | rank = 1 22 | 23 | ranks.append(TowerRank(rank, "lac", comment, info.cellobservation_id)) 24 | 25 | return ranks 26 | -------------------------------------------------------------------------------- /icc/models/CellObservation.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from sqlalchemy.orm import relationship 3 | from functools import partial 4 | from icc.database import Base 5 | 6 | from UUID import id_column, UUID 7 | 8 | # Prevents setting Not Null on each NotNullColumn 9 | NotNullColumn = partial(Column, nullable=False) 10 | 11 | 12 | class CellObservation(Base): 13 | __tablename__ = 'cellobservations' 14 | id = id_column() 15 | cid = NotNullColumn(Integer) 16 | arfcn = NotNullColumn(Integer) 17 | freq = NotNullColumn(Float) 18 | lac = NotNullColumn(Integer) 19 | mcc = NotNullColumn(Integer) 20 | mnc = NotNullColumn(Integer) 21 | # ccch_conf = ccch_conf 22 | power = NotNullColumn(Integer) 23 | s_rank = NotNullColumn(Integer, default=0) 24 | # neighbours = neighbours 25 | # cell_arfcns = cell_arfcns 26 | scan_id = NotNullColumn(UUID(), ForeignKey('scans.id')) 27 | scan = relationship("Scan", backref="cell_observations") 28 | celltowerscans = relationship('CellTowerScan') 29 | -------------------------------------------------------------------------------- /icc/models/CellTowerScan.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from functools import partial 3 | from icc.database import Base 4 | from sqlalchemy.orm import relationship 5 | 6 | from UUID import id_column, UUID 7 | 8 | # Prevents setting Not Null on each NotNullColumn 9 | NotNullColumn = partial(Column, nullable=False) 10 | 11 | 12 | class CellTowerScan(Base): 13 | __tablename__ = 'celltowerscans' 14 | id = id_column() 15 | sample_rate = NotNullColumn(Float) 16 | rec_time_sec = NotNullColumn(Integer) 17 | timestamp = NotNullColumn(DateTime(True)) 18 | cellobservation_id = NotNullColumn(UUID(), ForeignKey('cellobservations.id')) 19 | cell_observation = relationship("CellObservation") 20 | 21 | def getCaptureFileName(self): 22 | return "celltowerscan_{}-cellobservation_{}-samplerate_{}-timestamp_{}".format(self.id, self.cellobservation_id, 23 | self.sample_rate, 24 | self.timestamp.isoformat()) 25 | -------------------------------------------------------------------------------- /report/sample.bib: -------------------------------------------------------------------------------- 1 | 2 | @online{slides, 3 | author = "Peter A", 4 | title = "PRIVACY-ENHANCING TECHNOLOGIES:PRIVACY IN IDENTITY MANAGEMENT – PART 2 \& APPLICATION: E-VOTING", 5 | year = "2016", 6 | 7 | 8 | } 9 | 10 | @online{ger, 11 | author = "Stefan Trost Media", 12 | title = "Character Frequency: German (Deutsch)", 13 | year = "2007", 14 | urlseen = "20-09-15", 15 | url = "http://www.sttmedia.com/characterfrequency-nederlands", 16 | } 17 | 18 | @online{dut, 19 | author = "Stefan Trost Media", 20 | title = "Character Frequency: Nederlands (Dutch)", 21 | year = "2007", 22 | urlseen = "20-09-15", 23 | url = "http://www.sttmedia.com/characterfrequency-nederlands", 24 | } 25 | 26 | @online{eng, 27 | author = "Stefan Trost Media", 28 | title = "Character Frequency: English", 29 | year = "2007", 30 | urlseen = "20-09-15", 31 | url = "http://www.sttmedia.com/characterfrequency-english", 32 | } 33 | @article{gcc, 34 | author = "Wagle, P. and Cowan, C.", 35 | title = "StackGuard: Simple Stack Smash Protection for 36 | GCC", 37 | journal ="GCC Developers Submit", 38 | url = "ftp://gcc.gnu.org/pub/gcc/summit/2003/Stackguard.pdf" 39 | } 40 | -------------------------------------------------------------------------------- /icc/detectors/cell_reselection_hysteresis.py: -------------------------------------------------------------------------------- 1 | from detector import Detector 2 | from icc.gsmpackets import GSMTap 3 | 4 | cell_reselection_hysteresis_lower_threshold = 6 # db 5 | cell_reselection_hysteresis_upper_threshold = 9 # db 6 | 7 | 8 | class CellReselectionHysteresisDetector(Detector): 9 | def handle_packet(self, data): 10 | p = GSMTap(data) 11 | if p.channel_type == 1 and p.payload.message_type == 0x1b: 12 | sys_info3 = p.payload.payload 13 | cell_reselection_hysteresis = sys_info3.cell_reselection_hysteresis * 2 14 | if cell_reselection_hysteresis <= cell_reselection_hysteresis_lower_threshold: 15 | self.update_rank(Detector.NOT_SUSPICIOUS, "low (%d dB) cell reselection hysteresis detected" % cell_reselection_hysteresis) 16 | elif cell_reselection_hysteresis <= cell_reselection_hysteresis_upper_threshold: 17 | self.update_rank(Detector.UNKNOWN, "medium (%d dB) cell reselection hysteresis detected" % cell_reselection_hysteresis) 18 | else: 19 | self.update_rank(Detector.SUSPICIOUS, "high (%d dB) cell reselection hysteresis detected" % cell_reselection_hysteresis) 20 | -------------------------------------------------------------------------------- /report/elsarticle-template.blg: -------------------------------------------------------------------------------- 1 | This is BibTeX, Version 0.99d (TeX Live 2014) 2 | Capacity: max_strings=35307, hash_size=35307, hash_prime=30011 3 | The top-level auxiliary file: elsarticle-template.aux 4 | The style file: elsarticle-num.bst 5 | I found no \citation commands---while reading file elsarticle-template.aux 6 | Database file #1: sample.bib 7 | You've used 0 entries, 8 | 2937 wiz_defined-function locations, 9 | 668 strings with 5099 characters, 10 | and the built_in function-call counts, 44 in all, are: 11 | = -- 0 12 | > -- 0 13 | < -- 0 14 | + -- 0 15 | - -- 0 16 | * -- 2 17 | := -- 23 18 | add.period$ -- 0 19 | call.type$ -- 0 20 | change.case$ -- 0 21 | chr.to.int$ -- 0 22 | cite$ -- 0 23 | duplicate$ -- 0 24 | empty$ -- 1 25 | format.name$ -- 0 26 | if$ -- 1 27 | int.to.chr$ -- 0 28 | int.to.str$ -- 0 29 | missing$ -- 0 30 | newline$ -- 8 31 | num.names$ -- 0 32 | pop$ -- 0 33 | preamble$ -- 1 34 | purify$ -- 0 35 | quote$ -- 0 36 | skip$ -- 1 37 | stack$ -- 0 38 | substring$ -- 0 39 | swap$ -- 0 40 | text.length$ -- 0 41 | text.prefix$ -- 0 42 | top$ -- 0 43 | type$ -- 0 44 | warning$ -- 0 45 | while$ -- 0 46 | width$ -- 0 47 | write$ -- 7 48 | (There was 1 error message) 49 | -------------------------------------------------------------------------------- /icc/detectors/cell_reselection_offset.py: -------------------------------------------------------------------------------- 1 | from detector import Detector 2 | from icc.gsmpackets import GSMTap 3 | 4 | cell_reselection_offset_lower_threshold = 0 # db 5 | cell_reselection_offset_upper_threshold = 25 # db 6 | 7 | 8 | class CellReselectionOffsetDetector(Detector): 9 | def handle_packet(self, data): 10 | p = GSMTap(data) 11 | if p.channel_type == 1 and p.payload.message_type == 0x1b: 12 | sys_info3 = p.payload.payload 13 | if sys_info3.selection_parameters_present == 1: 14 | cell_reselection_offset = sys_info3.cell_reselection_offset * 2 15 | if cell_reselection_offset <= cell_reselection_offset_lower_threshold: 16 | self.update_rank(Detector.NOT_SUSPICIOUS, "low (%d dB) cell reselection offset detected" % cell_reselection_offset) 17 | elif cell_reselection_offset <= cell_reselection_offset_upper_threshold: 18 | self.update_rank(Detector.UNKNOWN, "medium (%d dB) cell reselection offset detected" % cell_reselection_offset) 19 | else: 20 | self.update_rank(Detector.SUSPICIOUS, "high (%d dB) cell reselection offset detected" % cell_reselection_offset) 21 | -------------------------------------------------------------------------------- /icc/models/UUID.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import types 2 | from sqlalchemy.types import Binary 3 | from sqlalchemy.schema import Column 4 | from functools import partial 5 | 6 | import uuid 7 | 8 | # Prevents setting Not Null on each NotNullColumn 9 | NotNullColumn = partial(Column, nullable=False) 10 | 11 | 12 | # http://stackoverflow.com/a/812363 13 | class UUID(types.TypeDecorator): 14 | impl = Binary 15 | 16 | def __init__(self): 17 | self.impl.length = 32 18 | types.TypeDecorator.__init__(self, length=self.impl.length) 19 | 20 | def load_dialect_impl(self, dialect): 21 | return dialect.type_descriptor(self.impl) 22 | 23 | def process_bind_param(self, value, dialect=None): 24 | if value and isinstance(value, uuid.UUID): 25 | return value.bytes 26 | elif value and not isinstance(value, uuid.UUID): 27 | raise ValueError, 'value %s is not a valid uuid.UUID' % value 28 | else: 29 | return None 30 | 31 | def process_result_value(self, value, dialect=None): 32 | if value: 33 | return uuid.UUID(bytes=value) 34 | else: 35 | return None 36 | 37 | def is_mutable(self): 38 | return False 39 | 40 | 41 | def id_column(): 42 | import uuid 43 | return NotNullColumn(UUID(), primary_key=True, default=uuid.uuid4) 44 | -------------------------------------------------------------------------------- /icc/detector_manager.py: -------------------------------------------------------------------------------- 1 | from detectors.detector import Detector 2 | import socket 3 | 4 | class DetectorManager(): 5 | 6 | def __init__(self, udp_port): 7 | """ 8 | Parameters: 9 | hooks = list of functions to call with the received frame as argument 10 | """ 11 | self.udp_port = udp_port 12 | self.sock = None 13 | self.running = False 14 | self.detectors = [] 15 | 16 | def addDetector(self, detector): 17 | if not isinstance(detector, Detector) or self.running is True: 18 | print "Could not add Detector to DetectorManager" 19 | return 20 | self.detectors.append(detector) 21 | 22 | def start(self): 23 | 24 | UDP_IP = "127.0.0.1" 25 | self.sock = socket.socket(socket.AF_INET, # Internet 26 | socket.SOCK_DGRAM) # UDP 27 | self.sock.settimeout(1) 28 | self.sock.bind((UDP_IP, self.udp_port)) 29 | self.running = True 30 | print "detectormanager started" 31 | while self.running: 32 | try: 33 | data, addr = self.sock.recvfrom(1024) # buffer size is 1024 bytes 34 | for detector in self.detectors: 35 | detector.handle_packet(data) 36 | except socket.timeout: 37 | pass 38 | 39 | def stop(self): 40 | self.running = False 41 | self.sock.close() 42 | rankings = [] 43 | for detector in self.detectors: 44 | rankings.append(detector.on_finish()) 45 | return rankings 46 | -------------------------------------------------------------------------------- /icc/cellinfochecks/tower.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | from sqlalchemy import Column, String, Integer, Float, DateTime 3 | 4 | class Tower(declarative_base()): 5 | __tablename__ = 'towers' 6 | 7 | radio = Column(String, primary_key=True) 8 | mcc = Column(Integer, primary_key=True) 9 | net = Column(Integer, primary_key=True) #mnc 10 | area = Column(Integer, primary_key=True) #lac 11 | cell = Column(Integer, primary_key=True) 12 | unit = Column(String) 13 | lon = Column(Float) 14 | lat = Column(Float) 15 | range = Column(Integer) 16 | samples = Column(Integer) 17 | changeable = Column(Integer) 18 | created = Column(Integer) 19 | updated = Column(Integer) 20 | average_signal = Column(Integer) 21 | 22 | def __repr__(self): 23 | return "" % ( 27 | self.radio, 28 | self.mcc, 29 | self.net, 30 | self.area, 31 | self.cell, 32 | self.unit, 33 | self.lon, 34 | self.lat, 35 | self.range, 36 | self.samples, 37 | self.changeable, 38 | self.created, 39 | self.updated, 40 | self.average_signal) 41 | -------------------------------------------------------------------------------- /icc/aux/ChannelInfo.py: -------------------------------------------------------------------------------- 1 | class ChannelInfo(object): 2 | 3 | def __init__(self, arfcn, freq, cid, lac, mcc, mnc, ccch_conf, power, neighbours, cell_arfcns): 4 | self.arfcn = arfcn 5 | self.freq = freq 6 | self.cid = cid 7 | self.lac = lac 8 | self.mcc = mcc 9 | self.mnc = mnc 10 | self.ccch_conf = ccch_conf 11 | self.power = power 12 | self.neighbours = neighbours 13 | self.cell_arfcns = cell_arfcns 14 | self.cellobservation_id = None 15 | 16 | def get_verbose_info(self): 17 | i = " |---- Configuration: %s\n" % self.get_ccch_conf() 18 | i += " |---- Cell ARFCNs: " + ", ".join(map(str, self.cell_arfcns)) + "\n" 19 | i += " |---- Neighbour Cells: " + ", ".join(map(str, self.neighbours)) + "\n" 20 | return i 21 | 22 | def get_ccch_conf(self): 23 | if self.ccch_conf == 0: 24 | return "1 CCCH, not combined" 25 | elif self.ccch_conf == 1: 26 | return "1 CCCH, combined" 27 | elif self.ccch_conf == 2: 28 | return "2 CCCH, not combined" 29 | elif self.ccch_conf == 4: 30 | return "3 CCCH, not combined" 31 | elif self.ccch_conf == 6: 32 | return "4 CCCH, not combined" 33 | else: 34 | return "Unknown" 35 | 36 | def getKey(self): 37 | return self.arfcn 38 | 39 | def __cmp__(self, other): 40 | if hasattr(other, 'getKey'): 41 | return self.getKey().__cmp__(other.getKey()) 42 | 43 | #def __repr__(self): 44 | # return "ARFCN: %4u, Freq: %6.1fM, CID: %5u, LAC: %5u, MCC: %3u, MNC: %3u, Pwr: %3i" % (self.arfcn, self.freq/1e6, self.cid, self.lac, self.mcc, self.mnc, self.power) 45 | -------------------------------------------------------------------------------- /icc/detectors/a5_detector.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | from icc.gsmpackets import * 3 | from detector import Detector 4 | 5 | class A5Detector(Detector): 6 | 7 | 8 | def handle_packet(self, data): 9 | p = GSMTap(data) 10 | if p.payload.name is 'LAPDm' and p.payload.payload.name is 'GSMAIFDTAP' and p.payload.payload.payload.name is 'CipherModeCommand': 11 | if p.payload.payload.payload.cipher_mode & 1 == 0: 12 | self.update_rank(Detector.SUSPICIOUS * 10, 'A5/0 detected! NO ENCRYPTION USED!') 13 | else: 14 | cipher = p.payload.payload.payload.cipher_mode >> 1 15 | cipher = cipher & 15 # MASK cipher AND 00001111 to only use the first four bits 16 | if cipher == 0: 17 | self.update_rank(Detector.SUSPICIOUS, 'A5/1 detected') 18 | elif cipher == 1: 19 | self.update_rank(Detector.SUSPICIOUS, 'A5/2 detected') 20 | elif cipher == 2: 21 | self.update_rank(Detector.NOT_SUSPICIOUS, 'A5/3 detected') 22 | elif cipher == 3: 23 | self.update_rank(Detector.NOT_SUSPICIOUS, 'A5/4 detected') 24 | elif cipher == 4: 25 | self.update_rank(Detector.NOT_SUSPICIOUS, 'A5/5 detected') 26 | elif cipher == 5: 27 | self.update_rank(Detector.NOT_SUSPICIOUS, 'A5/6 detected') 28 | elif cipher == 6: 29 | self.update_rank(Detector.NOT_SUSPICIOUS, 'A5/7 detected') 30 | else: 31 | self.update_rank(Detector.UNKNOWN, 'cipher used %s:' % cipher) 32 | -------------------------------------------------------------------------------- /icc/cellinfochecks/tic.py: -------------------------------------------------------------------------------- 1 | import query_cell_tower as CellTower 2 | import math 3 | from icc.aux import TowerRank 4 | 5 | def calc_distance(lat1, lon1, lat2, lon2): 6 | # approximate radius of earth in km 7 | R = 6373.0 8 | 9 | rlat1 = math.radians(lat1) 10 | rlon1 = math.radians(lon1) 11 | rlat2 = math.radians(lat2) 12 | rlon2 = math.radians(lon2) 13 | 14 | dlon = rlon2 - rlon1 15 | dlat = rlat2 - rlat1 16 | 17 | a = math.sin(dlat / 2)**2 + math.cos(rlat1) * math.cos(rlat2) * math.sin(dlon / 2)**2 18 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 19 | 20 | return (R * c) * 1000 21 | 22 | def tic(found_list, current_lat=52.2311057, current_lon=6.8553815, range_multiplier=1, verbose=False): 23 | # Tower Information Consistency Check 24 | ranks = [] 25 | 26 | 27 | if len(found_list) > 0: 28 | print("Printing cell tower info and checking database....") 29 | for info in sorted(found_list): 30 | ## checking database information 31 | rank = 0 32 | comment = None 33 | #print info 34 | if verbose: 35 | print info.get_verbose_info() 36 | towers = CellTower.queryTower(info.mcc, info.mnc, info.lac, info.cid) 37 | if len(towers) > 0: 38 | tower = towers[0] 39 | distance = calc_distance(tower.lat, tower.lon, current_lat, current_lon) 40 | if distance > (tower.range * range_multiplier): 41 | comment = "Cell tower found in database, but in wrong location %d m (range %d m)" % (distance, tower.range) 42 | rank = 1 43 | else: 44 | comment = "Cell tower found in database and is in range" 45 | else: 46 | comment = "No match found in database" 47 | rank = 1 48 | 49 | ranks.append(TowerRank(rank, "tic", comment, info.cellobservation_id)) 50 | else: 51 | print("No cell towers found...") 52 | 53 | return ranks 54 | -------------------------------------------------------------------------------- /icc/detectors/tic.py: -------------------------------------------------------------------------------- 1 | from detector import Detector 2 | from icc.gsmpackets import GSMTap 3 | 4 | import icc.cellinfochecks.query_cell_tower as CellTower 5 | 6 | import math 7 | 8 | def calc_distance(lat1, lon1, lat2, lon2): 9 | # approximate radius of earth in km 10 | R = 6373.0 11 | 12 | rlat1 = math.radians(lat1) 13 | rlon1 = math.radians(lon1) 14 | rlat2 = math.radians(lat2) 15 | rlon2 = math.radians(lon2) 16 | 17 | dlon = rlon2 - rlon1 18 | dlat = rlat2 - rlat1 19 | 20 | a = math.sin(dlat / 2) ** 2 + math.cos(rlat1) * math.cos(rlat2) * math.sin(dlon / 2) ** 2 21 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 22 | 23 | return (R * c) * 1000 24 | 25 | 26 | class TIC(Detector, object): 27 | was_run = False 28 | 29 | def __init__(self, name, cellobs_id, current_lat=52.2311057, current_lon=6.8553815, range_multiplier=1): 30 | super(self.__class__, self).__init__(name, cellobs_id) 31 | self.current_lat = current_lat 32 | self.current_lon = current_lon 33 | self.range_multiplier = range_multiplier 34 | 35 | def handle_packet(self, data): 36 | p = GSMTap(data) 37 | 38 | if p.channel_type == 1 and p.payload.message_type == 0x1b: 39 | sys_info3 = p.payload.payload 40 | 41 | if self.was_run: 42 | return 43 | 44 | self.was_run = True 45 | 46 | towers = CellTower.queryTower(sys_info3.mcc, sys_info3.mnc, sys_info3.lac, sys_info3.cid) 47 | if len(towers) > 0: 48 | tower = towers[0] 49 | distance = calc_distance(tower.lat, tower.lon, self.current_lat, self.current_lon) 50 | if distance > (tower.range * self.range_multiplier): 51 | self.update_rank(Detector.UNKNOWN, "Cell tower found in database, but in wrong location %d m (range %d m)" % (distance, tower.range)) 52 | else: 53 | self.update_rank(Detector.NOT_SUSPICIOUS, "Cell tower found in database and is in range") 54 | else: 55 | self.update_rank(Detector.UNKNOWN, "No match found in database") 56 | -------------------------------------------------------------------------------- /icc/detectors/detector.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | from icc.gsmpackets import * 3 | from multiprocessing import Process 4 | from icc.aux import TowerRank 5 | 6 | 7 | class Detector: 8 | 9 | SUSPICIOUS = 2 10 | UNKNOWN = 1 11 | NOT_SUSPICIOUS = 0 12 | 13 | def __init__(self, name, cellobs_id): 14 | """ 15 | Parameters: 16 | """ 17 | self.s_rank = 0 18 | self.name = name 19 | self.comment = 'Not enough information found' 20 | self.cellobs_id = cellobs_id 21 | self.counter = 0 22 | 23 | def update_rank(self, new_s_rank, new_comment): 24 | if new_s_rank >= self.s_rank: 25 | self.s_rank = new_s_rank 26 | self.comment = new_comment 27 | 28 | def handle_packet(self, data): 29 | print ':'.join(x.encode('hex') for x in data) 30 | 31 | def on_finish(self): 32 | return TowerRank(self.s_rank, self.name, self.comment, self.cellobs_id) 33 | 34 | def start(self): 35 | self.process = Process(target=self.listen) 36 | self.process.start() 37 | 38 | def stop(self): 39 | if not self.process is None: 40 | self.process.terminate() 41 | 42 | if __name__ == '__main__': 43 | #parser = Parser(4729) 44 | #parser.listen() 45 | im_as_packet_dump = "02 04 01 00 03 f9 e2 00 00 1e 45 1d 02 00 04 00 2d 06 3f 10 0e 83 f9 7a c0 c5 02 00 c6 94 e0 a4 2b 2b 2b 2b 2b 2b 2b" 46 | #p = GSMTap("02 04 01 00 03 f9 e1 00 00 06 07 ba 02 00 08 00 2d 06 3f 10 0e 83 f9 7e 54 48 01 00 c6 94 aa 34 2b 2b 2b 2b 2b 2b 2b".replace(' ', '').decode('hex')) 47 | p = GSMTap(im_as_packet_dump.replace(' ', '').decode('hex')) 48 | print p.version 49 | print p.header_length 50 | print p.payload_type 51 | print p.timeslot 52 | print p.arfcn 53 | print p.signal_level 54 | print p.signal_noise_ratio 55 | print p.gsm_frame_number 56 | print p.channel_type 57 | print p.antenna_number 58 | print p.sub_slot 59 | print p.payload.message_type 60 | print hexdump(p) 61 | print type(p.payload) 62 | print hexdump(p.payload) 63 | #print p.payload.encode('hex') 64 | print(type(p.payload.payload)) 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python # 2 | */*/*.pyc 3 | */*.pyc 4 | *.pyc 5 | ### Windows ### 6 | # Windows image file caches 7 | Thumbs.db 8 | ehthumbs.db 9 | 10 | # Folder config file 11 | Desktop.ini 12 | 13 | # Recycle Bin used on file shares 14 | $RECYCLE.BIN/ 15 | 16 | # Windows Installer files 17 | *.cab 18 | *.msi 19 | *.msm 20 | *.msp 21 | 22 | # Windows shortcuts 23 | *.lnk 24 | 25 | 26 | ### OSX ### 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Icon must end with two \r 32 | Icon 33 | 34 | 35 | # Thumbnails 36 | ._* 37 | 38 | # Files that might appear in the root of a volume 39 | .DocumentRevisions-V100 40 | .fseventsd 41 | .Spotlight-V100 42 | .TemporaryItems 43 | .Trashes 44 | .VolumeIcon.icns 45 | 46 | # Directories potentially created on remote AFP share 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | 53 | 54 | ### Linux ### 55 | *~ 56 | 57 | # temporary files which can be created if a process still has a handle open of a deleted file 58 | .fuse_hidden* 59 | 60 | # KDE directory preferences 61 | .directory 62 | 63 | # Linux trash folder which might appear on any partition or disk 64 | .Trash-* 65 | 66 | 67 | ### PyCharm ### 68 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 69 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 70 | 71 | # User-specific stuff: 72 | .idea/workspace.xml 73 | .idea/tasks.xml 74 | .idea/dictionaries 75 | .idea/vcs.xml 76 | .idea/jsLibraryMappings.xml 77 | 78 | # Sensitive or high-churn files: 79 | .idea/dataSources.ids 80 | .idea/dataSources.xml 81 | .idea/dataSources.local.xml 82 | .idea/sqlDataSources.xml 83 | .idea/dynamic.xml 84 | .idea/uiDesigner.xml 85 | 86 | # Gradle: 87 | .idea/gradle.xml 88 | .idea/libraries 89 | 90 | # Mongo Explorer plugin: 91 | .idea/mongoSettings.xml 92 | 93 | ## File-based project format: 94 | *.iws 95 | 96 | ## Plugin-specific files: 97 | 98 | # IntelliJ 99 | /out/ 100 | 101 | # mpeltonen/sbt-idea plugin 102 | .idea_modules/ 103 | 104 | # JIRA plugin 105 | atlassian-ide-plugin.xml 106 | 107 | # Crashlytics plugin (for Android Studio and IntelliJ) 108 | com_crashlytics_export_strings.xml 109 | crashlytics.properties 110 | crashlytics-build.properties 111 | fabric.properties 112 | .idea/ 113 | -------------------------------------------------------------------------------- /SDR_INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # Installation of the SDR libaries for the DVB-T dongle 2 | 3 | The installation instructions presented here are copied from the GR_GSM wiki and slightly modified. 4 | The installation was tested on Kali Linux. 5 | 6 | ## Install gnuradio 7 | 8 | `apt-get install gnuradio gnuradio-dev` 9 | 10 | ## Install RTL_SDR 11 | 12 | `apt-get install rtl-sdr librtlsdr-dev` 13 | 14 | ## Install GrOsmoSDR 15 | 16 | `apt-get install osmo-sdr libosmosdr-dev` 17 | 18 | ## Install other prerequisites 19 | 20 | ``` 21 | sudo apt-get install cmake libboost-all-dev libcppunit-dev swig doxygen liblog4cpp5-dev python-scipy 22 | ``` 23 | ``` 24 | sudo apt-get install libusb-1.0.0 libusb-dev 25 | ``` 26 | 27 | ## Install libosmocore 28 | 29 | ``` 30 | sudo apt-get install cmake 31 | sudo apt-get install build-essential libtool shtool autoconf automake git-core pkg-config make gcc 32 | sudo apt-get install libpcsclite-dev libtalloc-dev 33 | git clone git://git.osmocom.org/libosmocore.git 34 | cd libosmocore/ 35 | autoreconf -i 36 | ./configure 37 | make 38 | sudo make install 39 | sudo ldconfig -i 40 | cd 41 | ``` 42 | 43 | ## Install GR_GSM 44 | 45 | ``` 46 | git clone https://github.com/ptrkrysik/gr-gsm.git 47 | cd gr-gsm 48 | mkdir build 49 | cd build 50 | cmake .. 51 | make 52 | sudo make install 53 | sudo ldconfig 54 | ``` 55 | Finally, we create the ~/.gnuradio/config.conf config file with nano ~/.gnuradio/config.conf. We add this two lines to it (in that GNU Radio can find custom blocks of gr-gsm): 56 | ``` 57 | [grc] 58 | local_blocks_path=/usr/local/share/gnuradio/grc/blocks 59 | ``` 60 | 61 | ## Set access to USB devices 62 | Plug in the RTL-SDR device and check it's ID with `lsusb` command. You will see something like this: 63 | ```sh 64 | Bus 001 Device 004: ID **0bda:2832** Realtek Semiconductor Corp. RTL2832U DVB-T 65 | Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter 66 | Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. 67 | Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 68 | ``` 69 | 70 | In our case ID of the RTL.SDR device is **0bda:2832**. Now we open a rules file: 71 | ```sh 72 | sudo nano /etc/udev/rules.d/20.rtlsdr.rules 73 | ``` 74 | ...and add this line to it: 75 | ```sh 76 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2832", GROUP="adm", MODE="0666", SYMLINK+="rtl_sdr" 77 | ``` 78 | If you are using several RTL-SDR devices, you can add several lines to this file. 79 | 80 | Restart udev and remove/plugin your DVB-T dongle. 81 | -------------------------------------------------------------------------------- /icc/cellinfochecks/neighbours.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from icc.aux import TowerRank 3 | 4 | 5 | class Mesh: 6 | def __init__(self): 7 | self.edges = {} 8 | 9 | def add_vertex(self, vertex): 10 | if vertex not in self.edges: 11 | self.edges[vertex] = set() 12 | 13 | def add_edge(self, edge): 14 | (src, dst) = edge 15 | assert src in self.edges 16 | self.edges[src].add(dst) 17 | 18 | def vertices(self): 19 | return self.edges.keys() 20 | 21 | def size(self): 22 | return len(self.edges) 23 | 24 | def find_submesh(self, start): 25 | submesh = Mesh() 26 | vertex_queue = deque([start]) 27 | while len(vertex_queue) != 0: 28 | current_vertex = vertex_queue.pop() 29 | submesh.add_vertex(current_vertex) 30 | if current_vertex in self.edges: 31 | for next_vertex in self.edges[current_vertex]: 32 | submesh.add_edge((current_vertex, next_vertex)) 33 | if next_vertex in self.edges and next_vertex not in submesh.edges: 34 | vertex_queue.append(next_vertex) 35 | return submesh 36 | 37 | def find_edges_from(self, vertex): 38 | edges = set() 39 | for dst in self.edges[vertex]: 40 | edges.add((vertex, dst)) 41 | return edges 42 | 43 | def find_edges_to(self, vertex): 44 | edges = set() 45 | for src in self.edges: 46 | for dst in self.edges[src]: 47 | if dst == vertex: 48 | edges.add((src, dst)) 49 | return edges 50 | 51 | def __repr__(self): 52 | return "" % str(self.edges) 53 | 54 | 55 | min_submash_size = 3 56 | 57 | 58 | def neighbours(found_list): 59 | ranks = [] 60 | info_map = {} 61 | mesh = Mesh() 62 | for info in sorted(found_list): 63 | info_map[info.arfcn] = info 64 | mesh.add_vertex(info.arfcn) 65 | for neighbour in info.neighbours: 66 | mesh.add_edge((info.arfcn, neighbour)) 67 | for vertex in mesh.vertices(): 68 | rank = 0 69 | comment = None 70 | if len(mesh.find_edges_from(vertex)) == 0: # TODO: can be 0 due to inconsistent tower scan 71 | rank = 2 72 | comment = "Cell '%s' has no neighbours" % vertex 73 | elif len(mesh.find_edges_to(vertex)) == 0: 74 | rank = 2 75 | comment = "Cell '%s' is not referenced in the network" % vertex 76 | elif mesh.find_submesh(vertex).size() < min_submash_size: 77 | rank = 1 78 | comment = "Cell only has few neighbours" 79 | else: 80 | comment = "Cell has neighbours and is referenced in the network" 81 | ranks.append(TowerRank(rank, 'neighbours', comment, info_map[vertex].cellobservation_id)) 82 | return ranks 83 | -------------------------------------------------------------------------------- /icc/gsmpackets.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | 3 | 4 | class GSMTap(Packet): 5 | name = "GSMTap header" 6 | fields_desc = [XByteField("version", 0), 7 | XByteField("header_length", 0), 8 | XByteField("payload_type", 0), 9 | XByteField("timeslot", 0, ), 10 | XShortField("arfcn", 0), 11 | XByteField("signal_level", 0), # Returns wrong value, should be signed 12 | XByteField("signal_noise_ratio", 0), 13 | XIntField("gsm_frame_number", 0), 14 | XByteField("channel_type", 0), 15 | XByteField("antenna_number", 0), 16 | XByteField("sub_slot", 0), 17 | XByteField("end_junk", 0)] 18 | 19 | def guess_payload_class(self, payload): 20 | if self.channel_type == 1: 21 | return BCCHCommon 22 | elif self.channel_type == 2: 23 | return CCCHCommon 24 | elif self.channel_type == 8: 25 | return LAPDm 26 | else: 27 | return Raw 28 | 29 | 30 | class LAPDm(Packet): 31 | name = "LAPDm" 32 | fields_desc = [XByteField("address_field", 0), 33 | XByteField("control_field", 0), 34 | XByteField("len_field", 0)] 35 | 36 | def guess_payload_class(self, payload): 37 | if self.control_field == 32: 38 | return GSMAIFDTAP 39 | else: 40 | return Raw 41 | 42 | 43 | class GSMAIFDTAP(Packet): 44 | name = "GSMAIFDTAP" 45 | fields_desc = [XByteField("rrmm", 0), 46 | XByteField("message_type", 0)] 47 | 48 | def guess_payload_class(self, payload): 49 | if self.message_type == 53: 50 | return CipherModeCommand 51 | elif self.message_type == 24: 52 | return IdentityRequest 53 | else: 54 | return Raw 55 | 56 | class BCCHCommon(Packet): 57 | name = "BCCHCommon" 58 | fields_desc = [XShortField("junk1", 0), 59 | XByteField("message_type", 0)] 60 | 61 | def guess_payload_class(self, payload): 62 | if self.message_type == 0x1b: 63 | return SystemInfoType3 64 | else: 65 | return Raw 66 | 67 | 68 | class SystemInfoType3(Packet): 69 | name = "SystemInfoType3" 70 | fields_desc = [XShortField("cid", 0), 71 | XBitField("mcc_1", 0, 4), 72 | XBitField("mcc_0", 0, 4), 73 | XBitField("mnc_0", 0, 4), 74 | XBitField("mcc_2", 0, 4), 75 | XBitField("mnc_2", 0, 4), 76 | XBitField("mnc_1", 0, 4), 77 | XShortField("lac", 0), 78 | X3BytesField("control_channel_description", 0), 79 | XByteField("cell_options", 0), 80 | XBitField("cell_reselection_hysteresis", 0 , 3), 81 | XBitField("other_cell_selection_parameters", 0, 13), 82 | X3BytesField("rach_control_parameters", 0), 83 | XBitField("selection_parameters_present", 0, 1), 84 | XBitField("cbq", 0, 1), 85 | XBitField("cell_reselection_offset", 0, 6)] 86 | 87 | def post_dissection(self, s): 88 | self.mcc = int(str(self.mcc_0) + str(self.mcc_1) + str(self.mcc_2)) 89 | self.mnc = int((str(self.mnc_0) if self.mnc_0 != 0xf else '') + str(self.mnc_1) + str(self.mnc_2)) 90 | 91 | # rest of packet is left out] 92 | 93 | 94 | class CipherModeCommand(Packet): 95 | name = "CipherModeCommand" 96 | fields_desc = [XByteField("cipher_mode", 0)] 97 | 98 | 99 | class IdentityRequest(Packet): 100 | name = "IdentityRequest" 101 | fields_desc = [XByteField("id_type", 0)] 102 | 103 | 104 | class CCCHCommon(Packet): 105 | name = "CCCHCommon" 106 | fields_desc = [XShortField("junk1", 0), 107 | XByteField("message_type", 0)] 108 | 109 | def guess_payload_class(self, payload): 110 | if self.message_type == 63: 111 | return ImmediateAssignment 112 | else: 113 | return Raw 114 | 115 | 116 | class ImmediateAssignment(Packet): 117 | name = "ImmediateAssignment" 118 | fields_desc = [XByteField("junk", 2), 119 | XByteField("packet_channel_description", 6) # Extract indivual bits for detailed information 120 | ] 121 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from icc.runner import Runner, createDatabase 2 | from icc.runner import listScans as lc 3 | from icc.aux.lat_log_utils import parse_dms 4 | import grgsm 5 | import click 6 | from icc.file_analyzer import FileAnalyzer 7 | from icc.runner import offlineDetection 8 | 9 | @click.group() 10 | @click.option('--ppm', '-p', default=0, help='frequency offset in parts per million, default 0') 11 | @click.option('--samplerate', '-sr', default=2e6, type=float, help='samplerate in Hz') 12 | @click.option('--gain', '-g', type=float, default=30.0) 13 | @click.option('--speed', '-s', type=int, default=4, help="determines the speed of the scanner, .i.e. the speed value is subtracted from the sampling time for each frequency") 14 | @click.pass_context 15 | def cli(ctx, samplerate, ppm, gain, speed): 16 | """ 17 | IMSI catcher detector. This program tries to find nearby IMSI catchers using a RTL_SDR device. 18 | """ 19 | if speed < 0 or speed > 5: 20 | print "Invalid scan speed.\n" 21 | raise click.Abort 22 | 23 | if (samplerate / 0.2e6) % 2 != 0: 24 | print "Invalid sample rate. Sample rate must be an even numer * 0.2e6" 25 | raise click.Abort 26 | 27 | ctx.obj['samplerate'] = samplerate 28 | ctx.obj['ppm'] = ppm 29 | ctx.obj['gain'] = gain 30 | ctx.obj['speed'] = speed 31 | 32 | @click.command() 33 | @click.option('--band', '-b', default="900M-Bands", help="select the band to scan. One of: E-GSM, P-GSM or R-GSM") 34 | @click.option('--rec_time_sec', '-r', default=10, help='if the analyze option is specified, sets the recording time for each tower analysis in seocnds') 35 | @click.option('--analyze' , '-a', is_flag=True, help='turns on anylis of each tower found of the scan. Will produce a capture file of each tower for duration specified with --rec_time_sec') 36 | @click.option('--detection' , '-d', is_flag=True, help='if the analyze option is specified, turns on IMSI catcher detection during analysis of each tower') 37 | @click.option('--location' , '-l', type=str, default='', help='current location used to mark a scan in Apple GPS minutes format') 38 | @click.option('--lat', type=float, help='latitude used to specify the scan location, is used by detectors that perform location based detection of IMSI catchers') 39 | @click.option('--lon', type=float, help='longitude used to specify the scan location, is used by detectors that perform location based detection of IMSI catchers') 40 | @click.option('--unmute', '-u', is_flag=True, help='if the analyze option is specified, unmutes the output during analysis which will show the output of your capture device when it starts') 41 | @click.option('--no_store', '-S', is_flag=True, help='Do not store scan data') 42 | @click.pass_context 43 | def scan(ctx, band, rec_time_sec, analyze, detection, location, lat, lon, unmute, no_store): 44 | """ 45 | Scans for nearby cell towers and analyzes each cell tower and perfroms IMSI catcher detection if both are enabled. 46 | Note: if no location is specified, analysis of found towers is off 47 | :param detection: determines if druing analysis the packet based detectors are run 48 | """ 49 | if band != "900M-Bands": 50 | if band not in grgsm.arfcn.get_bands(): 51 | print "Invalid GSM band\n" 52 | return 53 | 54 | if band == "900M-Bands": 55 | to_scan = ['P-GSM', 56 | 'E-GSM', 57 | 'R-GSM', 58 | #'GSM450', 59 | #'GSM480', 60 | #'GSM850', Nothing found 61 | 'DCS1800', #BTS found with kal 62 | 'PCS1900', #Nothing interesting 63 | ] 64 | else: 65 | to_scan = [band] 66 | 67 | 68 | args=ctx.obj 69 | 70 | try: 71 | loc = parse_dms(location) 72 | lat = loc[0] 73 | lon = loc[1] 74 | except: 75 | pass 76 | if lat is None or lon is None: 77 | print "Warning: no valid location specified. Cell tower consistency checks will be disabled in the analysis phase." 78 | 79 | if not analyze: 80 | print "Analysis of found towers is DISABLED. No online detection methods will be used." 81 | else: 82 | print "Analysis of found cell towers is ENABLED." 83 | if not detection: 84 | print "Online detection methods DISABLED." 85 | print "GSM bands to be scanned:\n" 86 | print "\t", "\n\t".join(to_scan) 87 | 88 | #Add scan to database 89 | # 90 | runner = Runner(bands=to_scan, sample_rate=args['samplerate'], ppm=args['ppm'], gain=args['gain'], speed=args['speed'], rec_time_sec=rec_time_sec, current_location=location, store_capture=not no_store) 91 | runner.start(lat, lon, analyze=analyze, detection=detection, mute=not unmute) 92 | 93 | @click.command(help='Prints the saved scans') 94 | @click.option('--limit', '-n', help='Limit the number of results returned', default=10) 95 | @click.option('--printscans/--no-printscans', default=False) 96 | def listScans(limit, printscans): 97 | lc(limit, printscans) 98 | 99 | @click.command() 100 | @click.option('--timeslot', default=0, type=int, help="Decode timeslot 0 - [timeslot]") 101 | @click.option('--chan_mode', default='SDCCH8', help="Channel mode to demap the timeslots other than t0") 102 | def detectOffline(chan_mode, timeslot): 103 | offlineDetection(chan_mode, timeslot) 104 | 105 | 106 | @click.command() 107 | @click.argument('filename', type=str) 108 | @click.option('--sample_rate', default=2e6) 109 | @click.option('--arfcn', default=1017) 110 | @click.option('--timeslot', default=0, help="Decode timeslot 0 - [timeslot]") 111 | @click.option('--chan_mode', default='SDCCH8', help="Channel mode to demap the timeslots other than t0") 112 | def analyzeFile(filename, sample_rate, arfcn, timeslot, chan_mode): 113 | udp_port = 4729 114 | fa = FileAnalyzer(filename, sample_rate, arfcn, timeslot=timeslot, chan_mode=chan_mode, udp_port=udp_port, verbose=True, connectToSelf=True) 115 | fa.start() 116 | fa.wait() 117 | fa.stop() 118 | 119 | @click.command() 120 | def createdb(): 121 | createDatabase() 122 | 123 | if __name__ == "__main__": 124 | cli.add_command(scan) 125 | cli.add_command(listScans) 126 | cli.add_command(createdb) 127 | cli.add_command(analyzeFile) 128 | cli.add_command(detectOffline) 129 | cli(obj={}) 130 | -------------------------------------------------------------------------------- /report/numcompress.sty: -------------------------------------------------------------------------------- 1 | %% 2 | %% This is file 'numcompress.sty', 3 | %% 4 | %% Copyright (C) 2009 River Valley Technologies 5 | %% 6 | %% 7 | %% This file may be distributed and/or modified under the 8 | %% conditions of the LaTeX Project Public License, either version 1.2 9 | %% of this license or (at your option) any later version. 10 | %% The latest version of this license is in 11 | %% http://www.latex-project.org/lppl.txt 12 | %% and version 1.2 or later is part of all distributions of LaTeX 13 | %% version 1999/12/01 or later. 14 | %% 15 | %% $Id: numcompress.sty 144 2009-10-08 04:04:13Z rishi $ 16 | %% 17 | %% $URL: http://lenova.river-valley.com/svn/elsbst/trunk/numcompress.sty $ 18 | %% 19 | \newif\ifdots \dotstrue 20 | \newif\ifnumcompress \numcompresstrue 21 | 22 | \DeclareOption{dots}{\global\dotstrue} 23 | \DeclareOption{nodots}{\global\dotsfalse} 24 | \DeclareOption{compress}{\global\numcompresstrue} 25 | \DeclareOption{nocompress}{\global\numcompressfalse} 26 | 27 | \ProcessOptions 28 | 29 | \def\removeDot#1{\def\tmp{#1}% 30 | \ifx\tmp\@empty\else\@removeDot#1\@nil\fi} 31 | 32 | \def\@removeDot#1\@nil{\edef\fchar{\expandafter\@car#1\@nil}% 33 | \edef\rchar{\expandafter\@cdr#1!\@nil}% 34 | \def\@xmltempa{.}\def\@xmltempb{!}% 35 | \ifx\fchar\@xmltempb\relax\else% 36 | \ifx\fchar\@xmltempa\relax\else% 37 | \fchar\ignorespaces\fi\removeDot{\rchar}\fi} 38 | 39 | 40 | \def\First[#1]{\csname First#1\endcsname} 41 | \def\Second[#1]{\csname Second#1\endcsname} 42 | 43 | \def\parseFirstPage#1{\@tempcnta=0 44 | \@tfor\@digits:=#1\do{% 45 | {\global\advance\@tempcnta by 1 46 | \expandafter\xdef\csname 47 | First\the\@tempcnta\endcsname{\@digits}% 48 | \xdef\flength{\the\@tempcnta}}}} 49 | 50 | \def\parseSecondPage#1{\@tempcnta=0 51 | \@tfor\@digits:=#1\do{% 52 | {\global\advance\@tempcnta by 1 53 | \expandafter\xdef\csname 54 | Second\the\@tempcnta\endcsname{\@digits}% 55 | \xdef\llength{\the\@tempcnta}}}} 56 | 57 | \newif\ifdissimilar\dissimilarfalse 58 | \def\checkequal#1#2{\edef\Farg{#1}\edef\Sarg{#2}% 59 | \edef\One{A}% 60 | \ifcat\One\Farg \relax\else% 61 | \ifdissimilar\Sarg\else% 62 | \ifnum\Farg=\Sarg\relax\else\Sarg\dissimilartrue\fi\fi\fi} 63 | % 64 | \let\@@fpage\@empty 65 | \let\@@lpage\@empty 66 | \def\fpage@compress#1{% 67 | \gdef\@@fpage{#1}% 68 | \edef\llength{0}% 69 | \parseFirstPage{#1}% 70 | \ifnum\flength=\llength% 71 | \gdef\@fpage{\@@fpage}% 72 | \gdef\@lpage{% 73 | \@ifundefined{Second1}{}{\checkequal{\First[1]}{\Second[1]}}% 74 | \@ifundefined{Second2}{}{\checkequal{\First[2]}{\Second[2]}}% 75 | \@ifundefined{Second3}{}{\checkequal{\First[3]}{\Second[3]}}% 76 | \@ifundefined{Second4}{}{\checkequal{\First[4]}{\Second[4]}}% 77 | \@ifundefined{Second5}{}{\checkequal{\First[5]}{\Second[5]}}% 78 | }% 79 | \else% 80 | \gdef\@fpage{\@@fpage}% 81 | \gdef\@lpage{\@@lpage}% 82 | \fi} 83 | 84 | \def\lpage@compress#1{% 85 | \gdef\@@lpage{#1}% 86 | \parseSecondPage{#1}% 87 | \ifnum\flength=\llength% 88 | \gdef\@fpage{\@@fpage}% 89 | \gdef\@lpage{% 90 | \edef\One{A}% 91 | \edef\xFirst{\First[1]}% 92 | \edef\xSecond{\Second[1]}% 93 | \ifcat\One\xSecond\relax% 94 | \ifx\xFirst\xSecond% 95 | \@ifundefined{Second1}{}{\checkequal{\First[1]}{\Second[1]}}% 96 | \@ifundefined{Second2}{}{\checkequal{\First[2]}{\Second[2]}}% 97 | \@ifundefined{Second3}{}{\checkequal{\First[3]}{\Second[3]}}% 98 | \@ifundefined{Second4}{}{\checkequal{\First[4]}{\Second[4]}}% 99 | \@ifundefined{Second5}{}{\checkequal{\First[5]}{\Second[5]}}% 100 | \else#1\fi% 101 | \else% 102 | \ifx\xFirst\xSecond% 103 | \@ifundefined{Second1}{}{\checkequal{\First[1]}{\Second[1]}}% 104 | \@ifundefined{Second2}{}{\checkequal{\First[2]}{\Second[2]}}% 105 | \@ifundefined{Second3}{}{\checkequal{\First[3]}{\Second[3]}}% 106 | \@ifundefined{Second4}{}{\checkequal{\First[4]}{\Second[4]}}% 107 | \@ifundefined{Second5}{}{\checkequal{\First[5]}{\Second[5]}}% 108 | \else#1\fi% 109 | \fi% 110 | }% 111 | \else 112 | \gdef\@fpage{\@@fpage}% 113 | \gdef\@lpage{% 114 | \edef\Targ{#1}% 115 | \edef\One{A}% 116 | \edef\xFirst{\First[1]}% 117 | \edef\xSecond{\Second[1]}% 118 | \ifx\xFirst\xSecond 119 | \ifcat\One\xSecond\relax\else\@@lpage\fi% 120 | \else#1\fi% 121 | }% 122 | \fi} 123 | 124 | \newwrite\xx 125 | \immediate\openout\xx=tmpbib.tex 126 | 127 | \gdef\@@@pages#1#2{\def\next{#2}% 128 | \immediate\write\xx{[\the\c@NAT@ctr.]\space [#1][#2]}% 129 | \fpage@compress{#1}%\ifx\next\@empty\relax\else 130 | \lpage@compress{#2}%\fi 131 | {\@fpage\ifx\next\@empty\relax\else 132 | --\@lpage\fi}\resetall} 133 | 134 | \def\mk@empty#1{\@tempcnta=1 135 | \loop\ifnum\@tempcnta<6 136 | \expandafter\let\csname#1\the\@tempcnta\endcsname\relax 137 | \advance\@tempcnta by 1 \repeat} 138 | \def\resetall{\let\@lpage\@empty\let\@fpage\@empty 139 | \def\flength{0}\def\llength{0}% 140 | \let\@@fpage\@empty\let\@@lpage\@empty 141 | \mk@empty{First}\mk@empty{Second}} 142 | 143 | 144 | \ifdots 145 | \gdef\xfnm[#1]{\unskip\space#1} 146 | \def\bibinfo#1#2{\@ifnextchar.{\@@bibinfo{#1}{#2}}{\@@@bibinfo{#1}{#2}}} 147 | \def\@@@bibinfo#1#2{\def\next{#1}% 148 | \def\@@@pg{pages}\def\@@@au{author}% 149 | \ifx\next\@@@pg\bibpages{#2}\else 150 | \ifx\next\@@@au\bibauthor{#2}\else 151 | #2\fi\fi} 152 | \def\@@bibinfo#1#2.{\def\next{#1}% 153 | \def\@@@pg{pages}\def\@@@au{author}% 154 | \ifx\next\@@@pg\bibpages{#2}.\else 155 | \ifx\next\@@@au\bibauthor{#2}\else 156 | #2.\fi\fi} 157 | \else 158 | \gdef\xfnm[#1]{\unskip\space\removeDot{#1}} 159 | \def\bibinfo#1#2{\def\next{#1}% 160 | \def\@@@pg{pages}\def\@@@au{author}% 161 | \ifx\next\@@@pg\bibpages{#2}\else 162 | \ifx\next\@@@au\bibauthor{#2}\else 163 | #2\fi\fi} 164 | \fi 165 | 166 | \ifnumcompress 167 | \def\bibpages#1{\@@bibpages#1\@nil} 168 | \def\@@bibpages#1--#2\@nil{\@@@pages{#1}{#2}} 169 | \else 170 | \def\bibpages#1{#1} 171 | \fi 172 | 173 | \def\bibauthor#1{#1} 174 | 175 | 176 | -------------------------------------------------------------------------------- /report/elsarticle-template.tex: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2007, 2008, 2009 Elsevier Ltd 3 | %% 4 | %% This file is part of the 'Elsarticle Bundle'. 5 | %% --------------------------------------------- 6 | %% 7 | %% It may be distributed under the conditions of the LaTeX Project Public 8 | %% License, either version 1.2 of this license or (at your option) any 9 | %% later version. The latest version of this license is in 10 | %% http://www.latex-project.org/lppl.txt 11 | %% and version 1.2 or later is part of all distributions of LaTeX 12 | %% version 1999/12/01 or later. 13 | %% 14 | %% The list of all files belonging to the 'Elsarticle Bundle' is 15 | %% given in the file `manifest.txt'. 16 | %% 17 | 18 | %% Template article for Elsevier's document class `elsarticle' 19 | %% with numbered style bibliographic references 20 | %% SP 2008/03/01 21 | %% 22 | %% 23 | %% 24 | %% $Id: elsarticle-template-num.tex 4 2009-10-24 08:22:58Z rishi $ 25 | %% 26 | %% 27 | \documentclass[preprint,12pt,3p]{elsarticle} 28 | 29 | %\documentclass[final,3p,times]{elsarticle} 30 | %% Use the option review to obtain double line spacing 31 | %% \documentclass[preprint,review,12pt]{elsarticle} 32 | 33 | %% Use the options 1p,twocolumn; 3p; 3p,twocolumn; 5p; or 5p,twocolumn 34 | %% for a journal layout: 35 | %% \documentclass[final,1p,times]{elsarticle} 36 | %% \documentclass[final,1p,times,twocolumn]{elsarticle} 37 | %% \documentclass[final,3p,times]{elsarticle} 38 | %% \documentclass[final,3p,times,twocolumn]{elsarticle} 39 | %% \documentclass[final,5p,times]{elsarticle} 40 | %% \documentclass[final,5p,times,twocolumn]{elsarticle} 41 | 42 | %% if you use PostScript figures in your article 43 | %% use the graphics package for simple commands 44 | %% \usepackage{graphics} 45 | %% or use the graphicx package for more complicated commands 46 | %% \usepackage{graphicx} 47 | %% or use the epsfig package if you prefer to use the old commands 48 | %% \usepackage{epsfig} 49 | 50 | %% The amssymb package provides various useful mathematical symbols 51 | \usepackage{amssymb} 52 | \usepackage{amsmath} 53 | \usepackage{graphicx} 54 | \usepackage{enumitem} 55 | \usepackage{mathtools} 56 | \usepackage[utf8]{inputenc} 57 | \usepackage{color} 58 | \usepackage[procnames]{listings} 59 | \usepackage[]{algorithm2e} 60 | 61 | 62 | \graphicspath{ {img/} } 63 | %% The amsthm package provides extended theorem environments 64 | %% \usepackage{amsthm} 65 | 66 | %% The lineno packages adds line numbers. Start line numbering with 67 | %% \begin{linenumbers}, end it with \end{linenumbers}. Or switch it on 68 | %% for the whole article with \linenumbers after \end{frontmatter}. 69 | %% \usepackage{lineno} 70 | 71 | %% natbib.sty is loaded by default. However, natbib options can be 72 | %% provided with \biboptions{...} command. Following options are 73 | %% valid: 74 | 75 | %% round - round parentheses are used (default) 76 | %% square - square brackets are used [option] 77 | %% curly - curly braces are used {option} 78 | %% angle - angle brackets are used