#!/usr/bin/python # sidlog.py ### CONFIGURATIONS _REFRESH_RATE = 1 # The seconds between displayer updates FOREVER = 1000000 # This is the setting for how long forever is considered ### HINTS #This program contains tags for bug-tracking, information, and development. # #If you know Python, and are up to the task, here is what you need to know. # #Read the code to get a feel for the style and method. Edit and incrementally #make changes ensuring functionality. Here are some tags to guide your journey. # #TODO: Denotes areas where further functions should be added. #BUG: Denotes an area where something is working, but not 100% #INFO: Denotes a tag that gives hints to the methodology of the snippet of code # #If you edit, please comment and follow the tagging standard! # # ~blerbl ### #################################################### ### BELOW IS THE PROGRAM, EDIT AT YOUR OWN PERIL ### #################################################### ### About this program ### _name = "sidlog" _version = 0.4 _date = "22 Jan 2013" _authors = "blerbl" _about = """ This tool listens for 802.11 probe requests. It then logs the SSIDs in a database. It will keep track of the associated client SSIDs, the date last seen, and other information about the data collected. """ ### Platforming ### import sys _supported = ['linux2',] _LINUX = 'linux2' _ENV = sys.platform if not _ENV in _supported: raw_input('[#]' + _ENV + " not supported");exit(0) if sys.version_info[0] != 2 or sys.version_info[1] <= 6: raw_input("[#]Must be run with Python 2.x minor version >= .6\n You are running" + sys.version); exit(1) ### Imports ### import os, logging, socket, time, sqlite3, threading, argparse, re from urllib import urlretrieve as pyget from zipfile import ZipFile as pyzip from Tkinter import * # SCAPY!!! The embedded installer is a little buggy logging.getLogger("scapy.runtime").setLevel(logging.ERROR) try: from scapy.all import * except ImportError: #TODO This assumes that the ImportError is because scapy isnt installed # this may not be true. Further checking is needed print( "[!] Scapy is required to run " + _name) ans = raw_input( "Would you like to install Scapy at this time[Y|n]") if not 'n' in ans.lower(): print "[+]Installing scapy..." print "[++]Getting File" try: t_a,t_b = pyget('http://scapy.net','scapy.zip') temp = pyzip(t_a) print "[++]Extracting..." temp.extractall("scapyout") temp.close() print "[++]Running setup" os.chdir("scapyout") os.chdir(os.listdir('.')[0]) cmd = "setup.py install" if _ENV==_LINUX: cmd = './' + cmd errno = os.system(cmd) if errno : print "[#]There was an error installing scapy" exit(1) print "[++]Cleaning up" os.chdir("..") os.chdir("..") cmd = "rm -rf scapyout" #BUG needs os agnostic command os.system(cmd) os.remove("scapy.zip") try: print "[++]Scapy Installed!" from scapy.all import * except Exception as e: print "[#]Problem still not resolved. Check other dependencies" exit(1) except Exception as e: print "[#]Could not complete scapy install" exit(1) else: print("[#]Scapy requires for operation of " + _name + " v" + str(_version)) print("[-]Exiting...") exit(0) ### Globals ### re_essid = re.compile("\A[0-9A-z_.'\" \-&]+\Z") #re_essid = re.compile("\A[ -z]+\Z") sys.running = False sys.verbose = 0 def Error(message, level=1): """ Use this method to throw fatal errors""" print("[!]"+message) exit(level) def validessid(name): """ Determine if the tuple 'tup' meets the current filter criteria @returns True - If the tuple satisfies the current displayer filter states False - If the tuple does not satisfy the current displayer filter state""" if name is None: return False elif re_essid.match(name): return True return False class SidlogGUI(Frame): """ This is the GUI object for sidlog. It is dependent on the listener object. Create this object like any other Tkinter object.""" def __init__(self, master=None): Frame.__init__(self, master) self.listener = Listener() #TODO Organize this file and make the different components easy to identify self.armed = False self.pairs = [] #TODO Add a self.presentation to hold the things being displayed. # This will separate reponsibility from the self.pairs which will # then take on the role of just storing all information and be liked to # the listener. It may not even need the pairs directive but this may take # reference load off the listener. self.pack(expand=1, fill=BOTH) #INFO The menus, like all the other components in the GUI, are grouped in the program like they # are spacially. menu_main = Menu(self) menu_file = Menu(menu_main, tearoff=0) #TODO Refresh available interfaces. Use in conjuction with the self.entry_iface todos #menu_file.add_command(label="Refresh Ifaces", command=self.query_available_ifaces) #self.add_separator() menu_file.add_command(label=" Exit ", command=self.quit2) menu_main.add_cascade(label="Options", menu=menu_file) menu_sort = Menu(menu_main, tearoff=0) menu_sort.add_command(label="Order by Client",command=lambda: self.displayer_order_column(0)) menu_sort.add_command(label="Order by ESSID", command=lambda: self.displayer_order_column(1)) #TODO This is for grouping with self.presentation. When ready uncomment #menu_sort.add_separator() #menu_sort.add_command(label="Group by Client", command=self.displayer_group_client) #menu_sort.add_command(label="Group by ESSID", command=self.displayer_group_essid) #menu_sort.add_command(label="No Grouping", command=self.displayer_group_none) menu_main.add_cascade(label="View", menu=menu_sort) menu_act = Menu(menu_main, tearoff=0) menu_act.add_command(label="Monitor Mode",command=self.set_mon) menu_act.add_command(label="Managed Mode",command=self.unset_mon) menu_main.add_cascade(label="Action", menu=menu_act) root.configure(menu=menu_main) self.createWidgets() self.listener.statout = self.var_stat #very important, binds the status area threading.Thread(None,self.displayer,'diplayer').start() self.stat("Welcome") def createWidgets(self): """ Create the different widgets that are members of the main display""" #INFO the naming standard is 'self._' #Frames #INFO This is to create the underlying grid self.frame_control = Frame(self) self.frame_control.pack(side=LEFT,fill=Y) self.frame_view = Frame(self, height=300) self.frame_view.pack(side=RIGHT, fill=BOTH, expand=1) #The viewing frame components scrollbary = Scrollbar(self.frame_view) scrollbarx = Scrollbar(self.frame_view,orient=HORIZONTAL) scrollbary.pack(side=RIGHT, fill=Y) scrollbarx.pack(side=BOTTOM, fill=X) self.disp_results = Listbox(self.frame_view, xscrollcommand=scrollbarx.set, yscrollcommand=scrollbary.set, height=17, width=30) self.disp_results.config(font = ("Courier",10)) self.disp_results.pack(side=LEFT, fill=BOTH,expand=1) scrollbary.config(command=self.disp_results.yview) scrollbarx.config(command=self.disp_results.xview) #TODO add a search bar to filter results #TODO separate the listviews into two. Use the displayer and self.presentation to keep inline # and to group. e.g. # | 00:11:22:33:44:55 | airweb, TehTubez, Monkeycheesepants # OR # | PANERA | 00:11:22:33:44:55, 55:44:33:22:11:00 # # Perhaps use the StrVars and calculate every time, only updateing on difs # Or use list, identifying new items base on them not being in self.pairs # then checking if and where they deserve to be in the displayer based on the # state of displayer (self.displayer_state_<> objects) #The control frame components self.lable_iface = Label(self.frame_control) self.lable_iface["text"] = "Interface" self.lable_iface.pack(side=TOP) #TODO this should be a dropdown of available interfaces. Updated by a refresh interfaces action # in Options self.entry_iface = Entry(self.frame_control) self.entry_iface.insert(0,"wlan0") self.entry_iface.pack(side=TOP) self.lable_dbase = Label(self.frame_control) self.lable_dbase["text"] = "Database" self.lable_dbase.pack(side=TOP) self.entry_dbase = Entry(self.frame_control) self.entry_dbase.insert(0,"sidlog.db") self.entry_dbase.pack(side=TOP) self.var_stat = StringVar(self, "","status") self.disp_stat = Listbox(self.frame_control, listvariable=self.var_stat, font=("Courier",8)) dispx = Scrollbar(self.disp_stat,orient=HORIZONTAL) dispx.pack(side=BOTTOM, fill=X) self.disp_stat.config(xscrollcommand=dispx.set) self.disp_stat.pack(fill=BOTH, expand=1) dispx.config(command=self.disp_stat.xview) self.disp_stat.state=DISABLED self.btn_load = Button(self.frame_control) self.btn_load["text"] = "Load Config" self.btn_load['command'] = self.loadconf self.btn_load.pack(side=LEFT,fill=X) self.btn_toggle = Button(self.frame_control) self.btn_toggle["text"] = "Start" self.btn_toggle["command"] = self.toggle self.btn_toggle["state"]=DISABLED self.btn_toggle.pack(side=LEFT,fill=X) def quit2(self): """ Ensures that the things that need shutdown get shutdown before their master quits""" #INFO If you have a thread with a loop or something of the sorts # be sure to terminate it here self.running=False self.listener.stop() self.quit() def set_mon(self): """ Puts the interface in the entry into monitor mode""" if not self.listener.state == Listener.STOPPED: self.stat("Stop listener first",1) return 1 iface = self.entry_iface.get() t_s = socket.socket() try: t_s.setsockopt(socket.SOL_SOCKET, 25, iface) cmds = ["ifconfig " + iface + " down","iwconfig " + iface + " mode monitor","ifconfig " + iface + " up"] try: for cmd in cmds: os.system(cmd) self.stat("<" + iface + "> Monitoring") except: self.stat("<" + iface + "> Mon failed",1) except: self.stat("<" +iface + "> not a valid",1) t_gtg = False finally: del t_s def unset_mon(self): """ Puts interface in the entry into manage mode""" if not self.listener.state == Listener.STOPPED: self.stat("Stop listener first",1) return 1 iface = self.entry_iface.get() t_s = socket.socket() try: t_s.setsockopt(socket.SOL_SOCKET, 25, iface) cmds = ["ifconfig " + iface + " down","iwconfig " + iface + " mode managed","ifconfig " + iface + " up"] try: for cmd in cmds: os.system(cmd) self.stat("<" + iface + "> Managed") except: self.stat("<" + iface + "> mon failed",1) except: self.stat("<" +iface + "> not a valid",1) t_gtg = False finally: del t_s def loadconf(self): """ Loads the interface, makes sure it is good, loads the database, makes sure it is good. Enables and disables fields and buttons.""" if self.armed: self.armed = False self.btn_toggle['state']=DISABLED self.btn_load['text']="Load Config" self.entry_iface['state']=NORMAL self.entry_dbase['state']=NORMAL self.stat("Unlocked") return t_gtg = True iface = self.entry_iface.get() dbase = self.entry_dbase.get() t_s = socket.socket() try: t_s.setsockopt(socket.SOL_SOCKET, 25, iface) except: self.stat("<" +iface + "> not a valid",1) t_gtg = False finally: del t_s self.listener.iface = iface #check database try: self.listener.set_db(dbase) self.pairs = [] except Exception as e: self.stat("Bad DB: " + dbase,1) t_gtg = False if t_gtg: self.armed = True self.btn_toggle['state']=NORMAL self.btn_load['text']=" Unlock " self.entry_iface['state']=DISABLED self.entry_dbase['state']=DISABLED self.stat("Configured") self.displayer_needrefresh = True def toggle(self): if self.listener.state == Listener.RUNNING: self.btn_toggle["text"] = "Start" self.btn_load["state"]=NORMAL self.listener.stop() elif self.listener.state == Listener.STOPPED: self.btn_toggle["text"] = "Stop" self.btn_load["state"]=DISABLED self.listener.start() def stat(self, message, value=0): values = ["[+]","[-]"] ct = time.strftime("%H:%M") message = ct + values[value] + message cur = self.var_stat.get() or "()" cur = eval(cur) self.var_stat.set((message,) + cur) #################################INFO################################## ### Displayer family of commands and states. Keep most of them here ### #TODO Determine which way is better, go with it, and annotate why (Currently using 1) #1) The listener stores to the database AND exports to the GUI e.g.: DB <-> Listener <-> GUI #2) The listener stores to the database and the GUI querys it. e.g.: Listener <-> DB <-> GUI #TODO?1 add commands to query the listener #TODO?2 add commands to query the database def displayer_order_column(self,by=0,descend = True): """ sort the display by the column indicated in "by" then and inverte is based on the boolean descend""" try: tsave = self.pairs if by ==0: self.pairs = sorted(self.pairs, key=lambda x: int(x[0].replace(':',''),16),reverse=descend) elif by == 1: self.pairs = sorted(self.pairs, key=lambda x: (x[by]or"").lower(),reverse=descend) else: self.pairs = sorted(self.pairs, key=lambda x: x[by],reverse=descend) self.displayer_needrefresh = True except Exception as e: self.stat("Sort by "+str(by)+" failed: "+str(e),1) self.pairs = tsave #make the sort atomic def displayer_initStates(self): """ initialize all the result displayer's constants """ self.displayer_needrefresh = True def displayer(self): """ Updates the displayed results and applies necessary filtering. This is a working method meant to be launched in a thread.""" #INFO Notice how a single call initialized the states for the displayer. self.displayer_initStates() displayed = [] #this may become self.presentation depending on how the GUI gets its info while sys.running: time.sleep(_REFRESH_RATE) #INFO by filtering newpairs, it saves us time one the displayer when display filters # and insertions become intensive new_pairs = filter( lambda x: not x in self.pairs, self.listener.pairs) self.pairs.extend(new_pairs) if self.displayer_needrefresh: self.disp_results.delete(0,END) displayed = [] self.displayer_needrefresh = False new_pairs = self.pairs #all pairs are new when refreshing for newpair in new_pairs: if (not newpair in displayed) and (not newpair is None) and validessid(newpair[1]): displayed.append(newpair) self.disp_results.insert(0,newpair[0] + " | " + newpair[1] ) #BUG there is a bug that lets weird SSIDS display still ################# END DISPLAYER SECTION ################# ######################################################### ### END OF SidlogGUI #################################################################################### class Listener: """ Listener object, used to capture packets and log them to a sqlite3 database""" STOPPED = 0 STARTING = 1 RUNNING = 2 STOPPING = 3 CLIENT_TABLE_TEST="INSERT INTO clients (mac,time) values ('00:00:00:00:00:00',0)" ESSIDS_TABLE_TEST="INSERT INTO essids (mac,name) values ('00:00:00:00:00:00','test')" CLIENT_TABLE_CREATE="CREATE TABLE clients (mac,time INTEGER)" ESSIDS_TABLE_CREATE="CREATE TABLE essids (sid INTEGER PRIMARY KEY AUTOINCREMENT,mac,name)" #TODO Export the Database definitions so that other objects can use them def __init__(self,db=None): self.state = Listener.STOPPED self.statout = sys.stdout self.t = None self.iface = '' self.pairs = [] self.clients = [] self.running = False if db: self.set_db(db) #use stat to update the current status output method as determined by self.statout. #this device is usually stdout for console and the disp_stat for the GUI def stat(self, message, level=0): levels = ["[+]","[-]"] if level ") def stop(self): self.state = Listener.STOPPING self.running = False if self.t: self.t.join() self.stat("Stopped...") if __name__ == "__main__": sys.running = True parser = argparse.ArgumentParser(description=_about,prog=_name) parser.add_argument('--version', action='version', version="%(prog)s v"+str(_version)) parser.add_argument('--headless','-',action="store_const",default=False,const=True) parser.add_argument('--verbose', '-v', action='count') parser.add_argument('--force', '-f', action="store_const", default=False, const=True) parser.add_argument("-d", nargs=1, default="./sidlog.db",help="The database to store results.") parser.add_argument("-i","--iface", nargs=1, help="The interface to sniff on.") args = ["",] if len(sys.argv) > 0: args = parser.parse_args(sys.argv[1:]) else: args = parser.parse_args(sys.argv) ###### Important variables and stuff gl_dbName = args.d[0] gl_force = args.force sys.verbose = args.verbose or 0 if not args.headless: ### HERE BE THE GUI root = Tk() root.title("Sidlog v" + str(_version)) app = SidlogGUI(master=root) try: app.mainloop() root.destroy() except KeyboardInterrupt as e: print "Bye!" except: sys.stderr.write("[+]GUI exited unexpectedly\n") sys.running = False finally: sys.running = False else: ### HERE BE THE COMMAND LINE LOGIC if len(sys.argv)==2: parser.parse_args(['-h',]) else: gl_iface = args.iface[0] listen = Listener(gl_dbName) #make sure user put in valid iface t_s = socket.socket() try: t_s.setsockopt(socket.SOL_SOCKET, 25, gl_iface) except: Error("<"+ gl_iface+"> not a valid interface") finally: del t_s # repond to forcing monitor if gl_force: try: os.system("ifconfig " + gl_iface + " down") os.system("iwconfig " + gl_iface + " mode monitor") os.system("ifconfig " + gl_iface + " up") except: print("Could not force iface into monitor mode") try: listen.iface = gl_iface listen.start() if sys.verbose == 0: print "My PID is " + str(os.getppid()) try: time.sleep(FOREVER) except: pass listen.stop() exit(0) elif sys.verbose == 1: try: rotator = ['\\','|','/','-'] i = 0 try: os.system('setterm -cursor off') except: pass while FOREVER > 0: FOREVER -= 1 i = (i + 1) % 4 time.sleep(1) info = "Capturing: "+str(listen.count())+"\t so far [" + rotator[i] + "] \r" print(info), sys.stdout.flush() except: listen.stop() exit(0) finally: try: os.system('setterm -cursor on') except: pass else: time.sleep(FOREVER) except KeyboardInterrupt: print "Bye!" except: sys.stderr.write("[!]Sidlog Exited Unexpectedly\n") finally: sys.running = False if gl_force: try: os.system("ifconfig " + gl_iface + " down") os.system("iwconfig " + gl_iface + " mode managed") os.system("ifconfig " + gl_iface + " up") except: pass