def __init__(self, auditor, attackers, attacker, victim, proj, oracle, test_id, service, test_name, verbose, kml=None, query_lim=None, speed_limit=None): """Initializes a Discovery attack Args: users: the users in our user pool proj : the projection used oracle: an instance of the ProximityOracle class, either DUDP RUDP or a custom oracle defined by the inherited service kml: a path of a kml file with the search area for the victim """ # FIXME add sleep times depending on query rate self.kmlparser = KMLParser(proj) # pass Auditor class self.auditor = auditor self.attackers_backup = list(attackers) self.attackers = attackers self.restart_times = 0 self.attacker = attacker self.victim = victim self.proj = proj self.oracle = oracle self.verbose = verbose self.test_id = test_id # if limit is None set it to infinity self.query_limit = query_lim if query_lim is not None else float('inf') # # Attack parameters # # grid size for calculations: can be modified by user in xxx_attack call self.grid_size = 20 # query no in the current attack self.attack_queries = 0 self.test_name = test_name self.service_name = service # # Create directories for attack # self.kml_dir = ''.join(os.getcwd() + "/" + self.KML_DIR) if not os.path.exists(self.kml_dir): os.makedirs(self.kml_dir) self.json_dir = ''.join(os.getcwd() + "/" + self.JSON_DIR) if not os.path.exists(self.json_dir): os.makedirs(self.json_dir) # # Set up victim and seaerth area # if kml is None: self.kml = ''.join(os.getcwd() + "/" + self.NY_METROPOLITAN) self.search_area = self.kmlparser.poly_from_kml(self.kml) else: if os.path.isfile(kml): self.kml = kml self.search_area = self.kmlparser.poly_from_kml(kml) else: raise SystemExit("No such kml file") # get random victim location in projected coordinates (vict_x, vict_y) = self.kmlparser.random_from_polygon(self.search_area, 1)[0] (vict_lon, vict_lat) = proj(vict_x, vict_y, inverse=True) vb.vb_print(self.verbose, "Placing victim at " + str([vict_lat, vict_lon])) # place victim (success, queries) = auditor.auditor_handled_place_at_coords(victim, vict_lat, vict_lon, test_id) self.attack_queries += queries if not success: raise SystemExit("Could not place victim")
class DiscoveryAttack(object): """Generic Attack class """ # KML of New York metropolitan area NY_METROPOLITAN = "files/data/ny_metropolitan.kml" # directory to hold kml files with # all polygonal areas used in the attack KML_DIR = "files/kml/" # directory to hold json object with # all the attack info per query JSON_DIR = "files/json/" # stop binary search is area is smaller than BINARY_STOP_AREA BINARY_STOP_AREA = 100 # at every step of the binary search, we expect to # reduce the active search region at least 10% # otherwise something is not right MIN_REDUCTION = 0.01 # json object to hold all # stages of the attack json_out = { # queries run during coverage # stage of the attack "coverage" : [], # queries run during DUDP # stage of the attack "DUDP" : [], # queries run during RUDP # stage of the attack "RUDP" : [], # location of victim as reported # by the attack "est_location" : [], # real location of victim based on # the coordinates passed to the # service on the last update "real_location" : [], } def __init__(self, auditor, attackers, attacker, victim, proj, oracle, test_id, service, test_name, verbose, kml=None, query_lim=None, speed_limit=None): """Initializes a Discovery attack Args: users: the users in our user pool proj : the projection used oracle: an instance of the ProximityOracle class, either DUDP RUDP or a custom oracle defined by the inherited service kml: a path of a kml file with the search area for the victim """ # FIXME add sleep times depending on query rate self.kmlparser = KMLParser(proj) # pass Auditor class self.auditor = auditor self.attackers_backup = list(attackers) self.attackers = attackers self.restart_times = 0 self.attacker = attacker self.victim = victim self.proj = proj self.oracle = oracle self.verbose = verbose self.test_id = test_id # if limit is None set it to infinity self.query_limit = query_lim if query_lim is not None else float('inf') # # Attack parameters # # grid size for calculations: can be modified by user in xxx_attack call self.grid_size = 20 # query no in the current attack self.attack_queries = 0 self.test_name = test_name self.service_name = service # # Create directories for attack # self.kml_dir = ''.join(os.getcwd() + "/" + self.KML_DIR) if not os.path.exists(self.kml_dir): os.makedirs(self.kml_dir) self.json_dir = ''.join(os.getcwd() + "/" + self.JSON_DIR) if not os.path.exists(self.json_dir): os.makedirs(self.json_dir) # # Set up victim and seaerth area # if kml is None: self.kml = ''.join(os.getcwd() + "/" + self.NY_METROPOLITAN) self.search_area = self.kmlparser.poly_from_kml(self.kml) else: if os.path.isfile(kml): self.kml = kml self.search_area = self.kmlparser.poly_from_kml(kml) else: raise SystemExit("No such kml file") # get random victim location in projected coordinates (vict_x, vict_y) = self.kmlparser.random_from_polygon(self.search_area, 1)[0] (vict_lon, vict_lat) = proj(vict_x, vict_y, inverse=True) vb.vb_print(self.verbose, "Placing victim at " + str([vict_lat, vict_lon])) # place victim (success, queries) = auditor.auditor_handled_place_at_coords(victim, vict_lat, vict_lon, test_id) self.attack_queries += queries if not success: raise SystemExit("Could not place victim") def _update_attacker(self): """Get a new attacker from the available users """ if len(self.attackers) == 0: vb.vb_print(self.verbose, " *** RUN OUT OF ATTACKERS - RESTARTING ***", "UDP", True) self.attackers = self.attackers_backup self.restart_times += 1 vb.vb_print(self.verbose, " *** updating attacker ***", "UDP", True) self.attacker = self.attackers.pop() # sleep for some period sleep(10) def _log_kml(self, msg, polygon): """Outputs a kml in @KML_DIR """ output_kml = self.kml_dir + self.service_name + "_" + self.test_name output_kml += str(self.test_id) + "_q_" + str(self.restart_times) + "_" output_kml += str(self.attack_queries) + "_" + msg + ".kml" kml = self.kmlparser.kml_from_poly(polygon, output_kml) return kml def __get_candidate_dist(self, distance, rounding_class): """Depending on the rounding class type, inverse rounding of distance accordingly and return the candidate values for the real distance Returns distance in km """ [[min_r, max_r], rounding, family] = rounding_class # should never occur, but just to be safe if distance is None: return None if family == const.ROUNDING.UP: # distance has been rounded up, thus it could be # all anything in [dist - rounding, dist] if distance > rounding: return [distance - rounding, distance] else: return [0, distance] elif family == const.ROUNDING.DOWN: # distance has been rounded up, thus it could be # all anything in [dist, dist + rounding] return [distance, distance + rounding] elif family == const.ROUNDING.BOTH: if distance > rounding: return [distance - rounding, distance + rounding] else: return [0, distance + rounding] def __get_ring(self, minR): """Asks the proximity oracle and creates a ring respectively If we are in the base rounding class, we switch to binary """ # we expect to get an answer from the oracle # loop if there is an error and change attacker dist = None attempts = 0 while dist is None: # get distance and queries from the oracle (dist, queries) = self.oracle.in_proximity(self.attacker, self.victim, self.test_id) sleep(2) # increase total queries self.attack_queries += queries # increase queries if dist is None and attempts > 5: self._update_attacker() attempts += 1 # at this poing we god a distance from the proximity oracle # see in which rounding class this oracle belongs to for round_class in sorted([cl[0] for cl in self.oracle.round_cl]): # get the ranges for which this rounding class is active # e.x. from 100 to 200m --> [0.1, 0.2] [small_radius, big_radius] = round_class if dist >= small_radius and dist <= big_radius: vb.vb_print(self.verbose, "Distance returned: " + str(dist), "UDP", True) for cl in self.oracle.round_cl: if cl[0] == round_class: # see what the real distance might be (in km) real_dist = self.__get_candidate_dist(dist, cl) break if real_dist is None: return None vb.vb_print(self.verbose, "Candidate distances: " + str(real_dist), "UDP", True) attacker_location = self.attacker.loc # if we got a rounding class create a ring else return None # round_class should have the proper value from the last iteration # in the for loop. FIXME messy ring = cells.ring(attacker_location[0], attacker_location[1], float(real_dist[0]) * 1000, float(real_dist[1]) * 1000, self.proj) # return minimum distance and ring return real_dist, ring def _place_at_coords(self, attacker, lat, lon, test_id): """Attempt to update attacker location until we succeed """ while True: vb.vb_print(self.verbose, "Placing user at " + str(lat) + ", " + str(lon), "UDP", True) query_id = int(time()) res = self.auditor.auditor_handled_place_at_coords(attacker, lat, lon, test_id, query_id) # sleep until location is updated sleep(2) # add queries regardless of whether we failed self.attack_queries += res[1] if res[0] is False: # if for any reason update failed, change attacker self._update_attacker() else: return res def _run_trilateration(self, rounding_classes): """Runs a variance of the trilateration attack on the search_area Args: rouding_classes: a list of intervals with the radiuses Returns: A polygon in projected coordinates containing the target """ vb.vb_print(self.verbose, "Running trilateration", "UDP", True) # get minimum radius in the rounding classes MIN_R = sorted([cl[0] for cl in rounding_classes])[0][0] #initially the intersection is the whole area inter = self.search_area # place the attacker in the center if self.attacker.loc is None or self.attacker.loc[0] is None: starting_loc = cells.poly_centroid(inter, self.proj) self._place_at_coords(self.attacker, starting_loc[0], starting_loc[1], self.test_id) if self.oracle is None: self.oracle = apo.RoundingProximityOracle(self.auditor, rounding_classes, self.verbose) # initially get a rough estimate with 3 queries from # three locations at a distance of 120 degrees. # Initiate intersection rings rings = [] for i in range(3): # get a ring and check if we are switching to binary ring_response = self.__get_ring(MIN_R) if ring_response is None: raise SystemExit("victim not found") else: distance_range, ring = ring_response self._log_kml("ring", ring) rings.append(ring) #XXX no need to check for multipolygon in case of DUDP # as we are working inside a polygonal area inter_new = None if inter.geom_type == "MultiPolygon": for p in inter: cut_inter = p.intersection(ring) if inter_new is None: inter_new = cut_inter else: inter_new = inter_new.union(cut_inter) else: inter_new = inter.intersection(ring) # update the intersection inter = ring if inter_new.is_empty else inter_new # log kml files self.json_out["RUDP"].append({"query": self.attack_queries, "ring": self._log_kml("ring", ring), "active_area": self._log_kml("inter", inter), }) # update attacker location dist = float(distance_range[0] + distance_range[1]) / 2 # calculate new location for the next query new_loc = earth.point_on_earth(self.attacker.loc[0], self.attacker.loc[1], dist, i * 120) self._place_at_coords(self.attacker, new_loc[0], new_loc[1], self.test_id) # switch to binary return inter def _run_coverage(self, disk_radii): """Runs coverage algorithm on search_area disk_radii contains the list of available radii by the service This routine attempts to cover the search_area with as few disks as possible for the given set of disk_radii and then queries the proximity oracle for each of the disks until that returns true. Once the proximity oracle is true (the target is found in the disk) the routine returns the respective disk Args: disk_radii: the list of available radii for the disks used by the service in km Returns: A polygon in projected coordinates with the disk containing the target. """ vb.vb_print(self.verbose, "Running Coverage", "UDP", True) grid = cells.Cells(self.proj) # get largest radius and try to create a grid # with hexagons defined by r. sorted_radii = sorted(disk_radii, reverse=True) disk_radius = sorted_radii[0] # pdict is a dictionary with all the grid points. # If no keys are found the radius r is too big to # create a grid in the search area FIXME should we go for # binary directly the moment we have one succesful query? while len(grid.pdict.keys()) == 0: vb.vb_print(self.verbose, "Attempting to create grid with R=" + str(disk_radius), "UDP", True) # Attempt to construct grid with this radius grid.construct_grid_in_polygon(self.search_area, disk_radius * 1000) try: # get the next smaller radius in case this disk_radius = sorted_radii[next(x[0] for x in enumerate(sorted_radii) if x[1] < disk_radius)] except StopIteration: # Normally this should never trigger with the default kml raise SystemExit("Can't create grid with the available radii") vb.vb_print(self.verbose, "|--> Success!", "UDP", True) idx = sorted_radii.index(disk_radius) # Take previous r which is what the grid was made with # since we have already decreased this once disk_radius = sorted_radii[idx - 1] if self.oracle is None: self.oracle = apo.DiskProximityOracle(self.auditor, disk_radius, self.verbose) else: # set this radius in the oracle self.oracle.set_radius(disk_radius) # create the oracle grid_points = grid.pdict.keys() # FIXME we should update points based on the speed limits # currently let it randomly selects points. # # As a proper solution we should traverse the squares in a # consistent manner (like following a path zig-zag from left # to right then up then left etc). The problem is that if the # current location of the attacker is in the middle we would # have to do like a spiral which complicates stuff. So perhaps, # | # v # Replace the for loop with a while loop and after every iteration, # re-sort the remaining circles based on the distance from the # current point. Ugly but works and hopefully we won't have many circles random.shuffle(grid_points) for point in grid_points: # FIXME update depending on query limiting rates (lon, lat) = self.proj(float(point[0]), float(point[1]), inverse=True) # Place the user there respecting any speed constraints # Since the points are ordered based on the distance from # the current location, this is the closest point self._place_at_coords(self.attacker, lat, lon, self.test_id) # ask oracle until we get a response oracle_rspn = [None, None] attempts = 0 while oracle_rspn[0] is None: # ask the oracle if the victim is in proximity oracle_rspn = self.oracle.in_proximity(self.attacker, self.victim, self.test_id) # increase total queries of the attack self.attack_queries += oracle_rspn[1] if oracle_rspn[0] is None and attempts > 5: self._update_attacker() attempts +=1 circle = cells.circle(lat, lon, disk_radius * 1000, self.proj) self._log_kml("coverage", circle) self.json_out["coverage"].append({"query": self.attack_queries, "disk": [lat, lon, disk_radius * 1000]}) if oracle_rspn[0] is not None: if oracle_rspn[0] is True: vb.vb_print(self.verbose, "Found at " + vector.to_str([lat, lon]) + " !", "DUDP", True) return (circle, disk_radius) # if not found return None return None, None def _run_binary(self, inter, radius, grid_size=20): """Runs binary on area Args: @inter: the projected polygon in which we are running the binary attack algorithm @radius: the radius of the disk to perform the cuts """ vb.vb_print(self.verbose, "Running Binary", "UDP", True) last_inter_area = float('inf') while (inter.area > self.BINARY_STOP_AREA and self.attack_queries < self.query_limit): vb.vb_print(self.verbose, "Estimating cut", "UDP", True) # find projected coordinates that cut inter in half proj_coords = cells.cut(inter, self.proj, radius, grid_size) circle = Point(proj_coords[0], proj_coords[1]).buffer(radius * 1000) self.json_out["DUDP"].append({"query": self.attack_queries, "disk": self._log_kml("disk", circle), "active_area": self._log_kml("inter", inter), }) # calculate the coordinates for the new cut query (query_lon, query_lat) = self.proj(proj_coords[0], proj_coords[1], inverse=True) self._place_at_coords(self.attacker, query_lat, query_lon, self.test_id) if self.oracle is None: raise SystemExit("oracle should not be None after coverage") oracle_rspn = [None, None] attempts = 0 while oracle_rspn[0] is None: # ask the oracle if the victim is in proximity oracle_rspn = self.oracle.in_proximity(self.attacker, self.victim, self.test_id) # increase queries self.attack_queries += oracle_rspn[1] if oracle_rspn[0] is None and attempts > 5: self._update_attacker() attempts += 1 if oracle_rspn[0] is True: # if in proximity take the intersection inter_new = inter.intersection(circle) else: # else take the difference inter_new = inter.difference(circle) if inter_new.is_empty: print "\n\n\t ***WARNING!! EMPTY INTERSECTION***\n\n" inter = circle else: inter = inter_new # log kml self._log_kml("inter", inter) # if area is not reduced after intersection # break to avoid an infinite loop. area = inter.area if math.fabs(last_inter_area - area) < self.MIN_REDUCTION * area: vb.vb_print(self.verbose, "Area not significantly reduced ..stopping", "UDP", True) break else: last_inter_area = inter.area est_location = cells.poly_centroid(inter, self.proj) vb.vb_print(self.verbose, "Estimated Location: " + str(est_location), "UDP", True) real_est_distance = earth.distance_on_unit_sphere(self.victim.loc[0], self.victim.loc[1], est_location[0], est_location[1]) # convert to m real_est_distance *= 1000 vb.vb_print(self.verbose, "Distance from real loc: " + str(real_est_distance) + "m", "UDP", True) self.json_out["est_location"].append({"query": self.attack_queries, "area" : inter.area, "coords": [est_location[0], est_location[1]]}) self.json_out["real_location"].append({"query": -1, "coords": [self.victim.loc[0], self.victim.loc[1]]}) output_json = ''.join(os.getcwd() + "/" + self.JSON_DIR) output_json += self.service_name + "_UDP_" with open(output_json + str(self.test_id) + ".json", "w") as outfile: json.dump(self.json_out, outfile) return real_est_distance def dudp_attack(self, disk_radii, kml=None, grid_size=20): """Runs a DUDP attack Args: disk_radii: the list of available radii for the disks used by the service in km """ # TODO add documentation self.grid_size = grid_size # first limit search area into a single circle by running coverage # store this circle as the current intersection (inter) (inter, radius) = self._run_coverage(disk_radii) # now run binary # store the area of the last intersection to make # sure that after the cut the area is sufficiently reduced return self._run_binary(inter, radius) def rudp_attack(self, rounding_classes, kml=None, grid_size=20): """Runs an RUDP attack Args: rounding_classes: the rounding classes used by the service """ # first limit search area by running trilateration # using the rounding classes. @inter variable now # contains an area that is smaller than the minimum # radius in the rounding class so we can launch binary inter = self._run_trilateration(rounding_classes) min_rounding = sorted([cl[1] for cl in rounding_classes])[0] self._run_binary(inter, min_rounding)