├── .gitignore ├── LICENSE ├── README.md ├── Vulnerability_Analysis.pdf └── src ├── __init__.py ├── avd ├── __init__.py ├── core │ ├── __init__.py │ └── sliceEngine │ │ ├── __init__.py │ │ ├── graph.py │ │ ├── loopDetection.py │ │ └── slice.py ├── helper │ ├── __init__.py │ ├── binjaWrapper.py │ ├── decorators.py │ ├── drcov.py │ └── sources.py ├── loader.py ├── plugins │ ├── __init__.py │ ├── generic │ │ ├── BufferOverflow.py │ │ ├── IntegerOverflow.py │ │ ├── OutOfBounds.py │ │ ├── SignedAnalysis.py │ │ └── UninitializedVariable.py │ ├── optional │ │ └── LargeStackFrame.py │ └── special │ │ └── FindHeartbleed.py └── reporter │ ├── __init__.py │ └── vulnerability.py ├── main.py └── tests ├── test_plugins.py └── test_slice.py /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/**/usage.statistics.xml 5 | .idea/**/dictionaries 6 | .idea/**/shelf 7 | 8 | # Generated files 9 | .idea/**/contentModel.xml 10 | 11 | # Sensitive or high-churn files 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.local.xml 15 | .idea/**/sqlDataSources.xml 16 | .idea/**/dynamic.xml 17 | .idea/**/uiDesigner.xml 18 | .idea/**/dbnavigator.xml 19 | 20 | # Gradle 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # Gradle and Maven with auto-import 25 | # When using Gradle or Maven with auto-import, you should exclude module files, 26 | # since they will be recreated, and may cause churn. Uncomment if using 27 | # auto-import. 28 | # .idea/modules.xml 29 | # .idea/*.iml 30 | # .idea/modules 31 | 32 | # CMake 33 | cmake-build-*/ 34 | 35 | # Mongo Explorer plugin 36 | .idea/**/mongoSettings.xml 37 | 38 | # File-based project format 39 | *.iws 40 | 41 | # IntelliJ 42 | out/ 43 | 44 | # mpeltonen/sbt-idea plugin 45 | .idea_modules/ 46 | 47 | # JIRA plugin 48 | atlassian-ide-plugin.xml 49 | 50 | # Cursive Clojure plugin 51 | .idea/replstate.xml 52 | 53 | # Crashlytics plugin (for Android Studio and IntelliJ) 54 | com_crashlytics_export_strings.xml 55 | crashlytics.properties 56 | crashlytics-build.properties 57 | fabric.properties 58 | 59 | # Editor-based Rest Client 60 | .idea/httpRequests 61 | 62 | # Android studio 3.1+ serialized cache file 63 | .idea/caches/build_file_checksums.ser 64 | 65 | 66 | 67 | # Byte-compiled / optimized / DLL files 68 | __pycache__/ 69 | *.py[cod] 70 | *$py.class 71 | 72 | # C extensions 73 | *.so 74 | 75 | # Distribution / packaging 76 | .Python 77 | build/ 78 | develop-eggs/ 79 | dist/ 80 | downloads/ 81 | eggs/ 82 | .eggs/ 83 | lib/ 84 | lib64/ 85 | parts/ 86 | sdist/ 87 | var/ 88 | wheels/ 89 | pip-wheel-metadata/ 90 | share/python-wheels/ 91 | *.egg-info/ 92 | .installed.cfg 93 | *.egg 94 | MANIFEST 95 | 96 | # PyInstaller 97 | # Usually these files are written by a python script from a template 98 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 99 | *.manifest 100 | *.spec 101 | 102 | # Installer logs 103 | pip-log.txt 104 | pip-delete-this-directory.txt 105 | 106 | # Unit test / coverage reports 107 | htmlcov/ 108 | .tox/ 109 | .nox/ 110 | .coverage 111 | .coverage.* 112 | .cache 113 | nosetests.xml 114 | coverage.xml 115 | *.cover 116 | .hypothesis/ 117 | .pytest_cache/ 118 | 119 | # Translations 120 | *.mo 121 | *.pot 122 | 123 | # Django stuff: 124 | *.log 125 | local_settings.py 126 | db.sqlite3 127 | 128 | # Flask stuff: 129 | instance/ 130 | .webassets-cache 131 | 132 | # Scrapy stuff: 133 | .scrapy 134 | 135 | # Sphinx documentation 136 | docs/_build/ 137 | 138 | # PyBuilder 139 | target/ 140 | 141 | # Jupyter Notebook 142 | .ipynb_checkpoints 143 | 144 | # IPython 145 | profile_default/ 146 | ipython_config.py 147 | 148 | # pyenv 149 | .python-version 150 | 151 | # celery beat schedule file 152 | celerybeat-schedule 153 | 154 | # SageMath parsed files 155 | *.sage.py 156 | 157 | # Environments 158 | .env 159 | .venv 160 | env/ 161 | venv/ 162 | ENV/ 163 | env.bak/ 164 | venv.bak/ 165 | 166 | # Spyder project settings 167 | .spyderproject 168 | .spyproject 169 | 170 | # Rope project settings 171 | .ropeproject 172 | 173 | # mkdocs documentation 174 | /site 175 | 176 | # mypy 177 | .mypy_cache/ 178 | .dmypy.json 179 | dmypy.json 180 | 181 | # Pyre type checker 182 | .pyre/ 183 | 184 | 185 | 186 | *~ 187 | *.swp 188 | *.bndb 189 | *.pyc 190 | *.out 191 | *.com 192 | *.class 193 | *.dll 194 | *.exe 195 | *.o 196 | *.so 197 | src/tests/bin 198 | src/tests/juliet 199 | src/tests/oob 200 | src/tests/SliceEngine 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Traxes 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zeno Framework 2 | 3 | # Installation 4 | ``` 5 | cd ~ 6 | git clone https://github.com/Traxes/zeno --recursive 7 | sudo pip3 install termcolor tqdm 8 | git clone https://github.com/Z3Prover/z3 --recursive 9 | cd z3 10 | python3 scripts/mk_make.py --python 11 | cd build 12 | make 13 | sudo make install 14 | cd ~/zeno 15 | ``` 16 | 17 | # Usage 18 | ``` 19 | ~/zeno$ PYTHONPATH=$PYTHONPATH:$HOME/zeno python3 src/main.py 20 | ``` 21 | 22 | # Example 23 | For running all plugins on target.bin 24 | ``` 25 | ~/zeno$ PYTHONPATH=$PYTHONPATH:$HOME/zeno python3 src/main.py target.bin 26 | ``` 27 | 28 | 29 | # Documentation 30 | 31 | # TODO 32 | 33 | - Beauty Code 34 | - Prefix internal Methods with _ 35 | 36 | - Core 37 | - Slicing 38 | - Arch Support 39 | 40 | - Reporter 41 | - include into Plugin System 42 | 43 | - GUI 44 | 45 | - Extern Interface in DLLs 46 | 47 | - Plugins 48 | - BufferOverflow 49 | - Out of Bounds 50 | - IntegerOverflow 51 | - Format String 52 | - Uninitialized Memory 53 | - Graph Slicing! 54 | 55 | # False Positive 56 | - CWE457_Use_of_Uninitialized_Variable__char_pointer_17_bad() 57 | - False positive on goodG2B 58 | 59 | # DONE 60 | 61 | - Plugin loader 62 | 63 | # Resources & Similar Projects 64 | 65 | - https://github.com/cetfor/PaperMachete 66 | 67 | 68 | Buffer Overflows: 69 | gets <- always 70 | char buf[123]; std::cin>>buf 71 | -------------------------------------------------------------------------------- /Vulnerability_Analysis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Traxes/zeno/48e72e8884838a171a217336923beec2983853b5/Vulnerability_Analysis.pdf -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Traxes/zeno/48e72e8884838a171a217336923beec2983853b5/src/__init__.py -------------------------------------------------------------------------------- /src/avd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Traxes/zeno/48e72e8884838a171a217336923beec2983853b5/src/avd/__init__.py -------------------------------------------------------------------------------- /src/avd/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Traxes/zeno/48e72e8884838a171a217336923beec2983853b5/src/avd/core/__init__.py -------------------------------------------------------------------------------- /src/avd/core/sliceEngine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Traxes/zeno/48e72e8884838a171a217336923beec2983853b5/src/avd/core/sliceEngine/__init__.py -------------------------------------------------------------------------------- /src/avd/core/sliceEngine/graph.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import sys 3 | # TODO Strip down to only important parts 4 | # Credits https://github.com/endeav0r/binaryninja-varslice 5 | 6 | 7 | def set_intersection(sets): 8 | """ 9 | Takes a list of lists, and returns the intersection of those lists. 10 | """ 11 | if len(sets) < 1 : 12 | return sets 13 | intersection = copy.deepcopy(sets[0]) 14 | for s in sets : 15 | i = 0 16 | while i < len(intersection) : 17 | if intersection[i] not in s : 18 | del intersection[i] 19 | else : 20 | i = i + 1 21 | return intersection 22 | 23 | 24 | def set_equivalence(sets): 25 | """ 26 | Takes a list of lists, and returns True if all lists are equivalent, False 27 | otherwise. 28 | """ 29 | # An empty set is equivalent 30 | if len(sets) < 0: 31 | return True 32 | # Sets of differing length are obviously not equivalent 33 | l = len(sets[0]) 34 | for s in sets: 35 | if len(s) != l: 36 | return False 37 | 38 | sets_copy = copy.deepcopy(sets) 39 | for i in range(len(sets_copy)): 40 | sets_copy[i].sort() 41 | 42 | for i in range(len(sets_copy[0])): 43 | for j in range(len(sets_copy)): 44 | if sets_copy[0][i] != sets_copy[j][i]: 45 | return False 46 | 47 | return True 48 | 49 | 50 | def set_union(sets): 51 | if len(sets) < 0: 52 | return [] 53 | 54 | result = [] 55 | for s in sets: 56 | for ss in s: 57 | if ss not in result: 58 | result.append(ss) 59 | return result 60 | 61 | 62 | def find_loop_dominator(dominators, loop): 63 | dominator_sets = [] 64 | # Get dominator sets for all nodes in loop 65 | for d in dominators: 66 | if d in loop: 67 | dominator_sets.append(copy.deepcopy(dominators[d])) 68 | # Remove all indicies not in loop 69 | for s in dominator_sets: 70 | i = 0 71 | while i < len(s) : 72 | if s[i] not in loop: 73 | del s[i] 74 | else : 75 | i += 1 76 | # The one dominator all vertices have in common is the head of this loop 77 | if len(set_intersection(dominator_sets)): 78 | loop_dominator = set_intersection(dominator_sets)[0] 79 | else: 80 | return None 81 | return loop_dominator 82 | 83 | 84 | def edge_list_get_tail_index(edge_list, tail_index): 85 | """ 86 | Takes a list of edges and returns an edge if the tail_index matches the 87 | given index, or None otherwise. 88 | """ 89 | for edge in edge_list : 90 | if edge.tail_index == tail_index : 91 | return edge 92 | return None 93 | 94 | 95 | class Edge: 96 | """ 97 | This class represents a generic edge in a graph. It does not contain 98 | references to its head and tail directly, but instead indicies to the head 99 | and tail. 100 | 101 | You should not store references to this edge directly. 102 | """ 103 | def __init__(self, graph, index, head_index, tail_index, data=None): 104 | """ 105 | Create an edge. You should not call this directly. Call 106 | graph.add_edge() instead. 107 | """ 108 | self.graph = graph 109 | self.index = index 110 | self.head_index = head_index 111 | self.tail_index = tail_index 112 | self.data = data 113 | 114 | def head(self): 115 | """ 116 | Returns a reference to the head vertex of this edge. 117 | """ 118 | return self.graph.vertex_from_index(self.head_index) 119 | 120 | def tail(self): 121 | """ 122 | Returns a reference to the tail vertex of this edge. 123 | """ 124 | return self.graph.vertex_from_index(self.tail_index) 125 | 126 | 127 | class Vertex: 128 | """ 129 | This class represents a generic vertex in a graph. 130 | """ 131 | def __init__(self, graph, index, data=None): 132 | """ 133 | Creates a vertex. You should not call this directly. Call 134 | graph.add_vertex() instead. 135 | """ 136 | self.graph = graph 137 | self.index = index 138 | self.data = data 139 | 140 | def get_predecessor_indices(self): 141 | predecessor_edges = self.graph.get_edges_by_tail_index(self.index) 142 | return map(lambda e: e.head_index, predecessor_edges) 143 | 144 | def get_predecessors(self): 145 | return map(lambda i: self.graph.get_vertex_from_index(i), 146 | self.get_predecessor_indices()) 147 | 148 | def get_successor_indices(self): 149 | successor_edges = self.graph.get_edges_by_head_index(self.index) 150 | return map(lambda e: e.tail_index, successor_edges) 151 | 152 | def get_successors(self): 153 | return map(lambda i: self.graph.get_vertex_from_index(i), 154 | self.get_successor_indices()) 155 | 156 | 157 | class Graph: 158 | 159 | def __init__(self, entry_index=None): 160 | # When we create vertices, if an index is not specified, we increment 161 | # this to ensure we are creating unique vertex indicies 162 | self.next_vertex_index = -1000 163 | # A mapping of vertices by vertex index to vertex 164 | self.vertices = {} 165 | # When we create edges, we increment this to create unique edge indicies 166 | self.next_edge_index = 1 167 | 168 | # We keep references to the same edge in three different places to speed 169 | # up the searching for edges 170 | 171 | # A mapping of edges by edge index to edge 172 | self.edges = {} 173 | # A mapping of edges by head_index to edge 174 | self.edges_by_head_index = {} 175 | # A mapping of edges by tail_index to edge 176 | self.edges_by_tail_index = {} 177 | 178 | # An entry_index simplifies lots of stuff, like computing dominators 179 | self.entry_index = entry_index 180 | self.threshold = None 181 | 182 | def set_threshold(self, threshold): 183 | """ 184 | Defining a Threshold to limit really big Functions for faster performance. 185 | :param threshold: Number (usually 100000 is good) 186 | :return: 187 | """ 188 | self.threshold = threshold 189 | 190 | def add_edge(self, head, tail, data=None): 191 | """ 192 | Adds an edge to the graph by giving references to the head and tail 193 | vertices. 194 | This is just a wrapper for add_edge_by_indices. 195 | """ 196 | return self.add_edge_by_indices(head.index, tail.index, data) 197 | 198 | 199 | def add_edge_by_indices(self, head_index, tail_index, data=None): 200 | """ 201 | Adds an edge to the graph. Will fail if: 202 | 1) There is no vertex in the graph for head_index. 203 | 2) There is no vertex in the graph for tail_index. 204 | 3) An edge already exists from head -> tail. 205 | 206 | @param head_index The index of the head vertex. 207 | @param tail_index The index of the tail vertex. 208 | @param data Any data you would like associated with this edge. 209 | @return A reference to the new edge if it was created, or None on 210 | failure. 211 | """ 212 | 213 | # Ensure we have a valid head and tail 214 | if not head_index in self.vertices: 215 | return None 216 | if not tail_index in self.vertices: 217 | return None 218 | 219 | # If we already have an edge here, don't add a new one 220 | if head_index in self.edges_by_head_index \ 221 | and edge_list_get_tail_index(self.edges_by_head_index[head_index], tail_index): 222 | return None 223 | 224 | # Create our new edge 225 | index = self.next_edge_index 226 | edge = Edge(self, index, head_index, tail_index, data) 227 | 228 | # Add it to our dict of edges 229 | self.edges[index] = edge 230 | 231 | # Add this edge to our lists of edges by head_index and tail_index 232 | if not head_index in self.edges_by_head_index: 233 | self.edges_by_head_index[head_index] = [edge] 234 | else : 235 | self.edges_by_head_index[head_index].append(edge) 236 | 237 | if not tail_index in self.edges_by_tail_index: 238 | self.edges_by_tail_index[tail_index] = [edge] 239 | else : 240 | self.edges_by_tail_index[tail_index].append(edge) 241 | 242 | # Return the edge 243 | return edge 244 | 245 | def add_vertex(self, index=None, data=None): 246 | """ 247 | Adds a vertex to the graph. Index represents a desired index for this 248 | vertex, such as an address in a CFG, and data represents data you would 249 | like to associate with this vertex. If no index is given, one will be 250 | assigned. 251 | 252 | @param index A desired index for this vertex 253 | @param data Data you would like to associate with this vertex 254 | @return The newly created vertex, or None if the vertex could not be 255 | created. 256 | """ 257 | if index is None: 258 | index = self.next_vertex_index 259 | self.next_vertex_index += 1 260 | while self.vertices.has_key(index): 261 | index = self.next_vertex_index 262 | self.next_vertex_index += 1 263 | else: 264 | if index in self.vertices: 265 | return None 266 | self.vertices[index] = Vertex(self, index, data) 267 | return self.vertices[index] 268 | 269 | def compute_dominators(self): 270 | """ 271 | Returns a mapping of vertex indices to a list of dominators for that 272 | vertex. 273 | """ 274 | # We must have an entry_index to process dominators 275 | if self.entry_index is None: 276 | return None 277 | 278 | # Make a copy of this graph 279 | dag = self.directed_acyclic_graph() 280 | 281 | predecessors = dag.compute_predecessors() 282 | dominators = {} 283 | dominators[dag.entry_index] = [dag.entry_index] 284 | 285 | # queue of nodes to process 286 | queue = list(dag.get_vertex_from_index(dag.entry_index).get_successor_indices()) 287 | 288 | while len(queue) > 0: 289 | vertex_index = queue[0] 290 | queue = queue[1:] 291 | vertex = dag.get_vertex_from_index(vertex_index) 292 | 293 | # are all predecessors for this vertex_index set? 294 | predecessors_set = True 295 | for predecessor_index in predecessors[vertex_index]: 296 | if predecessor_index not in dominators: 297 | if predecessor_index not in queue: 298 | queue.append(predecessor_index) 299 | predecessors_set = False 300 | 301 | # if all predecessors are not set, they now come before this block 302 | # in the queue and will be set 303 | if not predecessors_set: 304 | queue.append(vertex_index) 305 | continue 306 | 307 | # all predecessors are set 308 | # This vertex's dominators are the intersection of all of its 309 | # immediate predecessors dominators 310 | doms = [] 311 | for predecessor_index in vertex.get_predecessor_indices() : 312 | doms.append(copy.deepcopy(dominators[predecessor_index])) 313 | 314 | dominators[vertex_index] = set_intersection(doms) 315 | dominators[vertex_index].append(vertex_index) 316 | 317 | # add successors to the queue 318 | for successor_index in vertex.get_successor_indices() : 319 | if successor_index not in queue : 320 | queue.append(successor_index) 321 | 322 | return dominators 323 | 324 | def compute_immediate_dominators(self): 325 | """ 326 | Returns a mapping of vertex nodes to their immediate dominators. 327 | """ 328 | immediate_dominators = {} 329 | 330 | dominators = self.compute_dominators() 331 | 332 | # For every vertex 333 | for vertex_index in dominators: 334 | # Get all of this vertex's strict dominators 335 | sdoms = dominators[vertex_index] 336 | # Well, strict dominators 337 | i = 0 338 | while i < len(sdoms) : 339 | if sdoms[i] == vertex_index : 340 | del sdoms[i] 341 | break 342 | i += 1 343 | # Determine which strict dominator does not dominate any of the 344 | # other dominators 345 | for sdom in sdoms: 346 | is_immediate_dominator = True 347 | for d in dominators[vertex_index]: 348 | # Don't check this strict dominator against itself 349 | if sdom == d : 350 | continue 351 | # And don't check this strict dominator against this vertex 352 | elif vertex_index == d: 353 | continue 354 | # Does this strict dominator exist in this dominator's dominators? 355 | if sdom in dominators[d]: 356 | is_immediate_dominator = False 357 | break 358 | if is_immediate_dominator: 359 | immediate_dominators[vertex_index] = sdom 360 | break 361 | 362 | return immediate_dominators 363 | 364 | def compute_predecessors (self): 365 | """ 366 | Returns a mapping of a vertex index to a list of vertex indices, where 367 | the key is given vertex and the value is a list of all vertices which 368 | are predecessors to that vertex. 369 | """ 370 | 371 | # Set our initial predecessors for each vertex 372 | predecessors = {} 373 | for vertex_index in self.vertices: 374 | vertex = self.vertices[vertex_index] 375 | predecessors[vertex_index] = list(vertex.get_predecessor_indices()) 376 | 377 | # We now do successive propogation passes until we no longer propogate 378 | queue = list(self.vertices.keys()) 379 | while len(queue) > 0: 380 | vertex_index = queue[0] 381 | queue = queue[1:] 382 | 383 | # for each predecessor of this vertex 384 | for predecessor_index in predecessors[vertex_index]: 385 | # Ensure all of these predecessor's are predecessors of this 386 | # vertex 387 | changed = False 388 | for pp_index in predecessors[predecessor_index]: 389 | if pp_index not in predecessors[vertex_index]: 390 | predecessors[vertex_index].append(pp_index) 391 | changed = True 392 | # if we changed, add all successors to the queue 393 | if changed: 394 | vertex = self.get_vertex_from_index(vertex_index) 395 | successor_indices = vertex.get_successor_indices() 396 | for s_index in successor_indices: 397 | if s_index not in queue: 398 | queue.append(s_index) 399 | 400 | return predecessors 401 | 402 | def directed_acyclic_graph(self): 403 | # DAGs must have an entry node 404 | if self.entry_index is None: 405 | return None 406 | 407 | # copy this graph 408 | graph = Graph(self.entry_index) 409 | for vertex_index in self.vertices: 410 | graph.vertices[vertex_index] = Vertex(graph, vertex_index) 411 | 412 | predecessors = self.compute_predecessors() 413 | 414 | # a set of already visited verticex indices 415 | visited = [] 416 | 417 | # A queue of indices to visit 418 | queue = [self.entry_index] 419 | 420 | valid_edges = [] 421 | 422 | while len(queue) > 0: 423 | vertex_index = queue[0] 424 | queue = queue[1:] 425 | 426 | # add this vertex_index to the visited set 427 | visited.append(vertex_index) 428 | 429 | # get the edges for all successors 430 | if vertex_index not in self.edges_by_head_index : 431 | continue 432 | 433 | edges = self.edges_by_head_index[vertex_index] 434 | for i in range(len(edges)): 435 | edge = edges[i] 436 | # if this edge would create a loop, skip it 437 | if edge.tail_index in predecessors[edge.head_index] and \ 438 | edge.tail_index in visited: 439 | continue 440 | 441 | # if we haven't seen this successor yet 442 | if edge.tail_index not in queue + visited: 443 | # add it to the queue 444 | queue.append(edge.tail_index) 445 | 446 | # this is a valid edge, add it 447 | graph.add_edge_by_indices(edge.head_index, edge.tail_index) 448 | 449 | return graph 450 | 451 | def detect_loops (self): 452 | """ 453 | Detects loops in the graph, and returns a set of sets, where each 454 | internal set is the vertex indices of a detected loop. 455 | 456 | Requires self.entry_index to be set. 457 | """ 458 | 459 | def loop_dfs(path, vertex_index): 460 | """ 461 | Takes a set of vertex indicies we have already walked, and the next 462 | vertex index to walk, and returns a set of sets, where each set is a 463 | detected loop 464 | @param path A set of indices we need to keep track of, but will not 465 | search. This should be in order of the search. 466 | @param vertex_index The next vertex_index to walk 467 | """ 468 | loops = [] 469 | 470 | # Grab the successor indices 471 | vertex = self.get_vertex_from_index(vertex_index) 472 | successor_indices = vertex.get_successor_indices() 473 | # For each successor 474 | for successor_index in successor_indices: 475 | # If this success is already in path, we have a loop 476 | if successor_index in path: 477 | # We should truncate the path prior to successor_index 478 | loop = copy.deepcopy(path) 479 | loop.append(vertex_index) 480 | loop = loop[loop.index(successor_index):] 481 | loops.append(loop) 482 | # Keep searching 483 | else: 484 | loops += loop_dfs(path + [vertex_index], successor_index) 485 | return loops 486 | 487 | loops = loop_dfs([], self.entry_index) 488 | 489 | 490 | # If we arrived at the same loop through different methods, we'll have 491 | # duplicates of the same loop, which we don't want. We need to remove 492 | # identical loop sets. 493 | for i in range(len(loops)): 494 | loops[i].sort() 495 | 496 | # This creates a pseudo-hash table of the loops and guarantees 497 | # uniqueness 498 | loop_hashes = {} 499 | for i in range(len(loops)) : 500 | loop_hashes[",".join(map(lambda x: str(x), loops[i]))] = loops[i] 501 | 502 | loops = loop_hashes.values() 503 | 504 | # We now have unique traces through loops, but multiple traces through 505 | # the same loop will show up as different loops. We want to merge traces 506 | # for the same loop. We do this by finding the head of the loop for each 507 | # trace, and then performing a union over the sets of vertices for loops 508 | # with identical heads. 509 | dominators = self.compute_dominators() 510 | 511 | loop_heads = {} 512 | for loop in loops: 513 | loop_dominator = find_loop_dominator(dominators, loop) 514 | if not loop_dominator: 515 | return list() 516 | if loop_dominator not in loop_heads: 517 | loop_heads[loop_dominator] = loop 518 | else: 519 | loop_head = loop_heads[loop_dominator] 520 | loop_heads[loop_dominator] = set_union([loop_head, loop]) 521 | 522 | return loop_heads.values() 523 | 524 | def get_edges_by_head_index (self, head_index) : 525 | """ 526 | Returns all edges who have a given head index. This is the same as the 527 | successor edges for a vertex by index. 528 | 529 | @param head_index The index of the vertex. 530 | @return A list of all edges with a head_index of head_index. An empty 531 | list will be returned if no such edges exist, including the case 532 | where a vertex with index head_index does not exist. 533 | """ 534 | if not head_index in self.edges_by_head_index: 535 | return [] 536 | return self.edges_by_head_index[head_index] 537 | 538 | def get_edges_by_tail_index(self, tail_index): 539 | """ 540 | Returns all edges who have a given tail index. This is the same as the 541 | predecessor edges for a vertex by index. 542 | 543 | @param tail_index The index of the vertex. 544 | @return A list of all edges with a tail_index of tail_index. An empty 545 | list will be returned if no such edges exist, including the case 546 | where a vertex with index tail_index does not exist. 547 | """ 548 | if not tail_index in self.edges_by_tail_index: 549 | return [] 550 | return self.edges_by_tail_index[tail_index] 551 | 552 | def get_vertex_from_index (self, index) : 553 | """ 554 | Returns a vertex with the given index. 555 | 556 | @param index The index of the vertex to retrieve. 557 | @return The vertex, or None if the vertex does not exist. 558 | """ 559 | if not index in self.vertices: 560 | return None 561 | return self.vertices[index] 562 | 563 | def get_vertices_data(self): 564 | return map(lambda x: x.data, [self.vertices[y] for y in self.vertices]) 565 | 566 | def _find_all_paths(self, node, nodes, node_count, result, cur=list()): 567 | if self.threshold: 568 | if len(result) > self.threshold: 569 | return 570 | if node not in nodes: 571 | nodes.append(node) 572 | cur = [node] 573 | 574 | if len(nodes) == node_count or len(list(self.get_vertex_from_index(node).get_successor_indices())) == 0: 575 | if nodes not in result: 576 | result.append(nodes) 577 | return 578 | 579 | if node not in cur: 580 | cur.append(node) 581 | 582 | for i in self.get_vertex_from_index(node).get_successor_indices(): 583 | if i not in cur: 584 | self._find_all_paths(i, nodes[:], node_count, result, cur[:]) 585 | 586 | def compute_all_paths(self): 587 | sys.setrecursionlimit(10000) 588 | paths = list() 589 | self._find_all_paths(list(self.vertices)[0], list(), len(self.vertices), paths) 590 | 591 | path_basic_blocks = [] 592 | for path in paths: 593 | tmp_blocks = [] 594 | for index in path: 595 | tmp_blocks.append(self.get_vertex_from_index(index).data.basic_block) 596 | path_basic_blocks.append(tmp_blocks) 597 | return path_basic_blocks -------------------------------------------------------------------------------- /src/avd/core/sliceEngine/loopDetection.py: -------------------------------------------------------------------------------- 1 | from .graph import * 2 | 3 | 4 | class InsAnalysis: 5 | """ 6 | A meta-instruction which we do analysis over 7 | """ 8 | def __init__ (self, basic_block, bb_index, il): 9 | """ 10 | Constructor for InsAnalysis 11 | @param basic_block The BBAnalysis this InsAnalysis belongs to. 12 | @param bb_index The index in BBAnalysis of this instruction. 13 | """ 14 | self.basic_block = basic_block 15 | self.bb_index = bb_index 16 | self.il = il 17 | self.written = {} 18 | self.read = {} 19 | 20 | def apply_ssa(self, in_variables): 21 | """ 22 | Takes a dict of identifiers to VariableAnalysis instances, and returns 23 | a dict of identifiers to VariableAnalysis instances of variables which 24 | are modified by in_variables. 25 | @param in_variables a dict of variable identifiers to VariableAnalysis 26 | which are valid before this instruction is executed. 27 | @return A dict of identifiers to VariableAnalysis instances, which are 28 | the state of all variables after this instruction. I.E. this is 29 | in_variables with identifiers that were written replaced with 30 | their new VariableAnalysis instances. 31 | """ 32 | 33 | # Get an instance of our SSA creator 34 | ssa = getSSA() 35 | 36 | written, read = il_registers(self.il) 37 | 38 | print(written, read, self.il, self.il.operation) 39 | 40 | # We don't want to modify the in_variables we were given. We can now 41 | # changes this up at will. 42 | in_variables = copy.deepcopy(in_variables) 43 | 44 | # We need to first go through read registers, and see if they are in 45 | # our in_variables. If not, we add those. 46 | for r in read: 47 | # If this read variable doesn't exist, create it and give it a 48 | # unique SSA identifier 49 | if r not in in_variables: 50 | self.read[r] = VariableAnalysis(r, ssa.new_ssa(r)) 51 | else: 52 | self.read[r] = copy.deepcopy(in_variables[r]) 53 | 54 | # No we go through written variables, and apply SSA to them 55 | written_ = {} 56 | for w in written: 57 | ww = VariableAnalysis(w, ssa.new_ssa(w)) 58 | written_[w] = ww 59 | 60 | # and we apply all of our read registers to our written registers 61 | for w in written_: 62 | for r in self.read: 63 | written_[w].dependencies.append(copy.deepcopy(r)) 64 | 65 | # save our written registers 66 | self.written = copy.deepcopy(written_) 67 | 68 | # now overwrite values in in_variables to create our result 69 | # written_ shouldn't have any references anywhere and should be a pure 70 | # copy. 71 | for w in written_: 72 | in_variables[w.identifier] = w 73 | 74 | return in_variables # in is the new out 75 | 76 | 77 | class BBAnalysis: 78 | """ 79 | This is a wrapper around a binary ninja basic block. We use this to track 80 | analysis around this block when creating a vertex in our graph. 81 | """ 82 | def __init__(self, basic_block): 83 | self.basic_block = basic_block 84 | self.instructions = [] 85 | for i in range(len(self.basic_block)) : 86 | self.instructions.append(InsAnalysis(self, i, self.basic_block[i])) 87 | 88 | def print_il_instructions(self): 89 | for ins in self.basic_block: 90 | print(ins.operation, ins) 91 | 92 | def read_written_registers(self): 93 | written_ = [] 94 | read_ = [] 95 | for il in self.basic_block: 96 | written, read = il_registers(il) 97 | print(written, read) 98 | for r in read: 99 | if r not in written_: 100 | read_.append(r) 101 | for w in written: 102 | written_.append(w) 103 | return read_, written_ 104 | 105 | def apply_ssa(self, in_variables): 106 | variables = in_variables 107 | for i in range(len(self.basic_block)): 108 | out_variables = self.instructions[i].apply_ssa(variables) 109 | for k in out_variables: 110 | variables[k] = out_variables[k] 111 | return variables 112 | 113 | 114 | def graph_function(func): 115 | graph = Graph(0) 116 | 117 | # get the low_level_il basic blocks 118 | basic_blocks = func.medium_level_il.basic_blocks 119 | 120 | # We are going to add each basic block to our graph 121 | for basic_block in basic_blocks: 122 | graph.add_vertex(basic_block.start, BBAnalysis(basic_block)) 123 | 124 | # Now we are going to add all the edges 125 | for basic_block in basic_blocks: 126 | for outgoing_edge in basic_block.outgoing_edges: 127 | target = outgoing_edge.target 128 | graph.add_edge_by_indices(basic_block.start, target.start, None) 129 | 130 | # Now return the graph 131 | return graph 132 | 133 | 134 | def loop_analysis(bb): 135 | graph = graph_function(bb.function) 136 | loops = graph.detect_loops() 137 | for loop in loops: 138 | if bb.start in loop: 139 | return True 140 | 141 | -------------------------------------------------------------------------------- /src/avd/core/sliceEngine/slice.py: -------------------------------------------------------------------------------- 1 | # Credits: Josh Watson @joshwatson for slicing parts 2 | from binaryninja import SSAVariable, Variable, MediumLevelILOperation, MediumLevelILFunction 3 | from src.avd.helper import binjaWrapper 4 | import sys 5 | 6 | 7 | class SlicedInstruction(object): 8 | def __init__(self, instr=None, function_index=None, sliced_variable=None, sliced_address=None): 9 | if instr is not None: 10 | self.instr = instr 11 | if function_index is not None: 12 | self.function_index = function_index 13 | if sliced_variable is not None: 14 | self.sliced_variable = sliced_variable 15 | if sliced_address is not None: 16 | self.sliced_address = sliced_address 17 | 18 | 19 | class SliceEngine(object): 20 | def __init__(self, args=None, bv=None): 21 | if args is not None: 22 | self._args = args 23 | if bv is not None: 24 | self._bv = bv 25 | 26 | # TODO Make forward slice to visit through non blacklisted functions 27 | @staticmethod 28 | def do_forward_slice(instruction, func): 29 | """ 30 | TODO 31 | :param instruction: 32 | :param func: 33 | :return: 34 | """ 35 | # if no variables written, return the empty set. 36 | if not instruction.ssa_form.vars_written: 37 | return set() 38 | 39 | instruction_queue = { 40 | use for var in instruction.ssa_form.vars_written if var.var.name 41 | for use in func.ssa_form.get_ssa_var_uses(var) 42 | } 43 | 44 | visited_instructions = {instruction.ssa_form.instr_index} 45 | 46 | while instruction_queue: 47 | visit_index = instruction_queue.pop() 48 | 49 | if visit_index is None or visit_index in visited_instructions: 50 | continue 51 | 52 | instruction_to_visit = func[visit_index] 53 | 54 | if instruction_to_visit is None: 55 | continue 56 | 57 | instruction_queue.update( 58 | ( 59 | use for var in instruction_to_visit.ssa_form.vars_written 60 | if var.var.name 61 | for use in func.ssa_form.get_ssa_var_uses(var) 62 | ) 63 | ) 64 | 65 | visited_instructions.add(visit_index) 66 | 67 | return visited_instructions 68 | 69 | # TODO Rework a new function to filter for single variables hitting functions 70 | @staticmethod 71 | def do_forward_slice_with_variable(instruction, function): 72 | """ 73 | TODO 74 | :param instruction: 75 | :param function: 76 | :return: 77 | """ 78 | # if no variables written, return the empty set. 79 | if not instruction.ssa_form.vars_written: 80 | return set() 81 | 82 | instruction_queue = {} 83 | 84 | for var in instruction.ssa_form.vars_written: 85 | if var.var.name: 86 | for use in function.ssa_form.get_ssa_var_uses(var): 87 | instruction_queue.update({use: var}) 88 | 89 | visited_instructions = [(instruction.ssa_form.instr_index, None)] 90 | 91 | while instruction_queue: 92 | 93 | visit_index = instruction_queue.popitem() 94 | 95 | if visit_index is None or visit_index[0] in visited_instructions: 96 | continue 97 | 98 | instruction_to_visit = function[visit_index[0]] 99 | 100 | if instruction_to_visit is None: 101 | continue 102 | 103 | for var in instruction_to_visit.ssa_form.vars_written: 104 | if var.var.name: 105 | for use in function.ssa_form.get_ssa_var_uses(var): 106 | instruction_queue.update({use: var}) 107 | 108 | visited_instructions.append(visit_index) 109 | 110 | return visited_instructions 111 | 112 | def handle_backward_slice_function_fast(self, func, index): 113 | """ 114 | Fast Function for Faster Progress 115 | :param func: 116 | :param index: 117 | :return: 118 | """ 119 | for ref in func.source_function.view.get_code_refs(func.source_function.start): 120 | previous_function = func.source_function.view.get_function_at(ref.function.start).medium_level_il 121 | calling_instr = previous_function[previous_function.get_instruction_start(ref.address)] 122 | new_slice_variable = calling_instr.ssa_form.vars_read[index] 123 | return self.do_backward_slice_with_variable(calling_instr, previous_function.ssa_form, new_slice_variable) 124 | 125 | def handle_backward_slice_function(self, func, index, recursion_limit): 126 | if self._args.fast: 127 | return self.handle_backward_slice_function_fast(func, index) 128 | else: 129 | return self.handle_backward_slice_function_precise(func, index, recursion_limit) 130 | 131 | def handle_backward_slice_function_precise(self, func, index, recursion_limit): 132 | """ 133 | Throughout function 134 | :param func: 135 | :param index: 136 | :param recursion_limit: 137 | :return: 138 | """ 139 | visited_instructions = list() 140 | for ref in func.source_function.view.get_code_refs(func.source_function.start): 141 | #for ref in func.source_function.view.get_code_refs(func.current_address): 142 | # Avoid referencing the same variable from the function 143 | if (ref.function.start, index) in recursion_limit: 144 | continue 145 | if ref.function.start == func.current_address: 146 | # If its referencing itself we skip it 147 | continue 148 | recursion_limit.append((ref.function.start, index)) 149 | calling_instr = binjaWrapper.get_medium_il_instruction(self._bv, ref.address) 150 | #previous_functions = self._bv.get_code_refs( 151 | # binjaWrapper.get_mlil_function(self._bv, ref.function.start).source_function.start 152 | #) 153 | #for previous_function in previous_functions: 154 | #previous_function = previous_function.function.medium_level_il 155 | #previous_function = func.source_function.view.get_function_at(ref.function.start).medium_level_il 156 | #calling_instr = previous_function[previous_function.get_instruction_start(ref.address)] 157 | # Skip if this was already sliced 158 | # TODO Remove all Hex occurances 159 | list_of_addresses = [(x.sliced_address, x.function_index) for x in visited_instructions] 160 | if (hex(calling_instr.address), calling_instr.instr_index) in list_of_addresses: 161 | continue 162 | if not calling_instr.ssa_form.vars_read: 163 | continue 164 | new_slice_variable = calling_instr.ssa_form.vars_read[index] 165 | visited_instructions += self.do_backward_slice_with_variable(calling_instr, calling_instr.function.ssa_form, new_slice_variable, recursion_limit) 166 | return visited_instructions 167 | 168 | def get_sources_of_variable(self, bv, var): 169 | # TODO Recreate this function.. it´s ugly 170 | if not var: 171 | return [] 172 | if isinstance(var, SSAVariable): 173 | var = var.var 174 | sources = [] 175 | if "arg" in var.name: 176 | sources.append(var.function.name) 177 | if isinstance(var.function, MediumLevelILFunction): 178 | func = var.function.ssa_form 179 | else: 180 | func = var.function.medium_level_il.ssa_form 181 | for bb in func: 182 | for instr in bb: 183 | for v in (instr.vars_read + instr.vars_written): 184 | if isinstance(v, Variable): 185 | if v.identifier == var.identifier: 186 | visited = self.do_forward_slice(instr, v.function.medium_level_il.ssa_form) 187 | for index in visited: 188 | call = v.function.medium_level_il.ssa_form[index] 189 | if call.operation == MediumLevelILOperation.MLIL_CALL_SSA: 190 | if hasattr(call.dest, "constant"): 191 | if bv.get_symbol_at(call.dest.constant): 192 | sources.append(bv.get_symbol_at(call.dest.constant).name) 193 | else: 194 | sources.append("sub_" + hex(call.dest.constant)) 195 | else: 196 | # TODO Relative Call.. skip until implemented 197 | pass 198 | # Resolv call.dest 199 | return sources 200 | 201 | @staticmethod 202 | def get_ssa_manual_var_uses(func, var): 203 | """ 204 | TODO 205 | :param func: 206 | :param var: 207 | :return: 208 | """ 209 | variables = [] 210 | for bb in func: 211 | for instr in bb: 212 | for v in (instr.vars_read + instr.vars_written): 213 | if v.identifier == var.identifier: 214 | variables.append(instr.instr_index) 215 | return variables 216 | 217 | @staticmethod 218 | def get_manual_var_uses(func, var): 219 | """ 220 | TODO 221 | :param func: 222 | :param var: 223 | :return: 224 | """ 225 | variables = [] 226 | for bb in func: 227 | for instr in bb: 228 | for v in (instr.vars_read + instr.vars_written): 229 | if v.identifier == var.identifier: 230 | variables.append(instr.instr_index) 231 | return variables 232 | 233 | @staticmethod 234 | def get_manual_var_uses_custom_bb(bb_paths, var): 235 | """ 236 | TODO implement only bb paths 237 | :param bb_paths: 238 | :param var: 239 | :return: 240 | """ 241 | return var.function.medium_level_il.get_var_definitions(var) + var.function.medium_level_il.get_var_uses(var) 242 | 243 | def get_sources(self, bv, ref, instr, n): 244 | """ 245 | TODO 246 | :param bv: 247 | :param ref: 248 | :param instr: 249 | :param n: 250 | :return: 251 | """ 252 | visited_src = self.do_backward_slice_with_variable( 253 | instr, 254 | binjaWrapper.get_mlil_function(bv, ref.address).ssa_form, 255 | binjaWrapper.get_ssa_var_from_mlil_instruction(instr, n), 256 | list() 257 | ) 258 | possible_sources = list() 259 | for sources in visited_src: 260 | possible_sources += self.get_sources_of_variable(bv, sources.sliced_variable) 261 | return list(set(possible_sources)) 262 | 263 | def get_sources2(self, bv, instr, var): 264 | visited_src = self.do_backward_slice_with_variable( 265 | instr, 266 | var.var.function.medium_level_il.ssa_form, 267 | var, 268 | list() 269 | ) 270 | possible_sources = list() 271 | for sources in visited_src: 272 | possible_sources += self.get_sources_of_variable(bv, sources.sliced_variable) 273 | return list(set(possible_sources)) 274 | 275 | def get_sources_with_mlil_function(self, bv, func, instr, n): 276 | """ 277 | TODO 278 | :param bv: 279 | :param func: 280 | :param instr: 281 | :param n: 282 | :return: 283 | """ 284 | slice_src, visited_src = self.do_backward_slice_with_variable( 285 | instr, 286 | func.medium_level_il, 287 | binjaWrapper.get_ssa_var_from_mlil_instruction(instr, n) 288 | ) 289 | 290 | return self.get_sources_of_variable(bv, slice_src) 291 | 292 | @staticmethod 293 | def get_var_from_register(bv, instr, n): 294 | """ 295 | TODO 296 | :param bv: 297 | :param instr: 298 | :param n: 299 | :return: 300 | """ 301 | mlil_function = binjaWrapper.get_mlil_function(bv, instr.address) 302 | ssa_var = instr.ssa_form.vars_read[n] 303 | return mlil_function[mlil_function.get_ssa_var_definition(ssa_var)] 304 | 305 | def do_backward_slice_with_variable(self, instruction, func, variable, recursion_limit): 306 | """ 307 | TODO 308 | :param instruction: 309 | :param func: in MLIL SSA Form: 310 | :param variable: the Variable to trace: 311 | :return: 312 | """ 313 | if not isinstance(variable, SSAVariable): 314 | # Bail out if no SSA Var 315 | return list() 316 | instruction_queue = list() 317 | first_instruction = SlicedInstruction( 318 | instruction.ssa_form, 319 | instruction.ssa_form.instr_index, 320 | variable, 321 | hex(instruction.ssa_form.address) 322 | ) 323 | if variable.var.name: 324 | instruction_queue.append(first_instruction) 325 | 326 | visited_instructions = [first_instruction] 327 | 328 | while instruction_queue: 329 | 330 | visit_index = instruction_queue.pop() 331 | 332 | if visit_index is None or len( 333 | [x for x in visited_instructions if x.function_index != visit_index.function_index]) < 0: 334 | continue 335 | 336 | instruction_to_visit = func[visit_index.function_index] 337 | 338 | if instruction_to_visit is None: 339 | continue 340 | 341 | # Special Case for a edge case in BN 342 | vars = list() 343 | if instruction_to_visit.operation == MediumLevelILOperation.MLIL_STORE_SSA: 344 | if variable in instruction_to_visit.vars_read: 345 | if instruction_to_visit.vars_read.index(variable): 346 | vars = instruction_to_visit.src.vars_read 347 | else: 348 | vars = instruction_to_visit.dest.vars_read 349 | else: 350 | vars = instruction_to_visit.ssa_form.vars_read 351 | else: 352 | vars = instruction_to_visit.ssa_form.vars_read 353 | 354 | for var in vars: 355 | if type(var) is not SSAVariable: 356 | if len([x for x in visited_instructions if x.sliced_address != hex(instruction_to_visit.address) 357 | or x.function_index != instruction_to_visit.instr_index]) > 0: 358 | visited_instructions.append(SlicedInstruction( 359 | instruction_to_visit.ssa_form, 360 | instruction_to_visit.ssa_form.instr_index, 361 | var, 362 | hex(instruction_to_visit.ssa_form.address) 363 | )) 364 | continue 365 | if var.var.name: 366 | if func.ssa_form.get_ssa_var_definition(var) is not None: 367 | tmp_instr = func[func.ssa_form.get_ssa_var_definition(var)] 368 | list_of_addresses = [(x.sliced_address, x.function_index) for x in visited_instructions] 369 | if (hex(tmp_instr.address), tmp_instr.instr_index) not in list_of_addresses: 370 | instruction_queue.append( 371 | SlicedInstruction( 372 | tmp_instr, 373 | func.ssa_form.get_ssa_var_definition(var), 374 | var, 375 | hex(tmp_instr.address) 376 | ) 377 | ) 378 | else: 379 | # Traverse Functions Backwards 380 | if len([x for x in visited_instructions if x.sliced_address != hex(instruction_to_visit.address) 381 | or x.function_index != instruction_to_visit.instr_index]) > 0: 382 | visited_instructions.append(SlicedInstruction( 383 | instruction_to_visit.ssa_form, 384 | instruction_to_visit.ssa_form.instr_index, 385 | var, 386 | hex(instruction_to_visit.ssa_form.address) 387 | )) 388 | # Prevent multiple entries 389 | list_of_addresses = [(x.sliced_address, x.function_index) for x in visited_instructions] 390 | for sliced in self.handle_backward_slice_function(func, var.var.index, recursion_limit): 391 | if (sliced.sliced_address, sliced.function_index) not in list_of_addresses: 392 | visited_instructions.append(sliced) 393 | 394 | if len([x for x in visited_instructions if x.sliced_address != visit_index.sliced_address 395 | or x.function_index != visit_index.function_index]) > 0: 396 | visited_instructions.append(visit_index) 397 | 398 | return visited_instructions 399 | -------------------------------------------------------------------------------- /src/avd/helper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Traxes/zeno/48e72e8884838a171a217336923beec2983853b5/src/avd/helper/__init__.py -------------------------------------------------------------------------------- /src/avd/helper/binjaWrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | These helper functions are usually one liner. Those conversions are usually not (yet) very well documented 3 | and thus, I do not forget them again :). 4 | """ 5 | import codecs 6 | 7 | 8 | def get_low_il_instruction(bv, addr): 9 | """ 10 | This helper function will return the vulnurable low level il Instruction 11 | :param addr: 12 | :return: 13 | """ 14 | # TODO check if this breaks with ARM or Mips ! 15 | return bv.arch.get_instruction_low_level_il_instruction(bv, addr) 16 | 17 | 18 | def get_medium_il_instruction(bv, addr): 19 | # Assuming that there is only one matching function for the given address. 20 | mff = bv.get_functions_containing(addr)[0].medium_level_il 21 | return mff[mff.get_instruction_start(addr)] 22 | 23 | 24 | def get_ssa_var_from_mlil_instruction(instr, i): 25 | return instr.ssa_form.vars_read[i] 26 | 27 | 28 | def get_mlil_function(bv, addr): 29 | # Assuming that there is only one matching function for the given address. 30 | return bv.get_functions_containing(addr)[0].medium_level_il 31 | 32 | def get_basic_block_from_instr(bv, addr): 33 | try: 34 | bb = bv.get_basic_blocks_at(addr)[0] 35 | return bb 36 | except IndexError: 37 | return None 38 | 39 | def get_constant_string(bv, addr): 40 | """ 41 | Returns the full string in memory 42 | :param bv: the BinaryView: 43 | :param addr: Address where the string is: 44 | :return string: 45 | """ 46 | str_len = 0 47 | curr = codecs.encode(bv.read(addr, 1), "hex").decode() 48 | while (curr != "2e") and (curr != "00"): 49 | str_len += 1 50 | curr = codecs.encode(bv.read(addr + str_len, 1), "hex").decode() 51 | return bv.read(addr, str_len).decode() 52 | -------------------------------------------------------------------------------- /src/avd/helper/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | class ArgumentValidationError(ValueError): 5 | """ 6 | Raised when the type of an argument to a function is not what it should be. 7 | """ 8 | 9 | def __init__(self, arg_num, func_name, accepted_arg_type): 10 | self.error = 'The {0} argument of {1}() is not a {2}'.format(arg_num, 11 | func_name, 12 | accepted_arg_type) 13 | 14 | def __str__(self): 15 | return self.error 16 | 17 | 18 | class InvalidArgumentNumberError(ValueError): 19 | """ 20 | Raised when the number of arguments supplied to a function is incorrect. 21 | Note that this check is only performed from the number of arguments 22 | specified in the validate_accept() decorator. If the validate_accept() 23 | call is incorrect, it is possible to have a valid function where this 24 | will report a false validation. 25 | """ 26 | 27 | def __init__(self, func_name): 28 | self.error = 'Invalid number of arguments for {0}()'.format(func_name) 29 | 30 | def __str__(self): 31 | return self.error 32 | 33 | 34 | class InvalidReturnType(ValueError): 35 | """ 36 | As the name implies, the return value is the wrong type. 37 | """ 38 | 39 | def __init__(self, return_type, func_name): 40 | self.error = 'Invalid return type {0} for {1}()'.format(return_type, 41 | func_name) 42 | 43 | def __str__(self): 44 | return self.error 45 | 46 | 47 | def ordinal(num): 48 | """ 49 | Returns the ordinal number of a given integer, as a string. 50 | eg. 1 -> 1st, 2 -> 2nd, 3 -> 3rd, etc. 51 | """ 52 | if 10 <= num % 100 < 20: 53 | return '{0}th'.format(num) 54 | else: 55 | ord = {1: 'st', 2: 'nd', 3: 'rd'}.get(num % 10, 'th') 56 | return '{0}{1}'.format(num, ord) 57 | 58 | 59 | def accepts(*accepted_arg_types): 60 | """ 61 | // Code from https://www.pythoncentral.io/validate-python-function-parameters-and-return-types-with-decorators/ 62 | A decorator to validate the parameter types of a given function. 63 | It is passed a tuple of types. eg. (, ) 64 | 65 | Note: It doesn't do a deep check, for example checking through a 66 | tuple of types. The argument passed must only be types. 67 | """ 68 | 69 | def accept_decorator(validate_function): 70 | # Check if the number of arguments to the validator 71 | # function is the same as the arguments provided 72 | # to the actual function to validate. We don't need 73 | # to check if the function to validate has the right 74 | # amount of arguments, as Python will do this 75 | # automatically (also with a TypeError). 76 | @functools.wraps(validate_function) 77 | def decorator_wrapper(*function_args, **function_args_dict): 78 | if len(accepted_arg_types) is not len(accepted_arg_types): 79 | raise InvalidArgumentNumberError(validate_function.__name__) 80 | 81 | # We're using enumerate to get the index, so we can pass the 82 | # argument number with the incorrect type to ArgumentValidationError. 83 | for arg_num, (actual_arg, accepted_arg_type) in enumerate(zip(function_args, accepted_arg_types)): 84 | if not type(actual_arg) is accepted_arg_type: 85 | # very cheeky way to get around the self parameter in classes 86 | if not hasattr(actual_arg, "bug_class"): 87 | ord_num = ordinal(arg_num + 1) 88 | raise ArgumentValidationError(ord_num, 89 | validate_function.__name__, 90 | accepted_arg_type) 91 | 92 | return validate_function(*function_args) 93 | 94 | return decorator_wrapper 95 | 96 | return accept_decorator 97 | -------------------------------------------------------------------------------- /src/avd/helper/drcov.py: -------------------------------------------------------------------------------- 1 | """ 2 | From the Lighthouse Plugin 3 | 4 | By Markus Gaasedelen @gaasedelen 5 | """ 6 | 7 | #!/usr/bin/python 8 | 9 | import os 10 | import sys 11 | import mmap 12 | import struct 13 | import re 14 | from ctypes import * 15 | 16 | #------------------------------------------------------------------------------ 17 | # drcov log parser 18 | #------------------------------------------------------------------------------ 19 | 20 | class DrcovData(object): 21 | """ 22 | A drcov log parser. 23 | """ 24 | def __init__(self, filepath=None): 25 | 26 | # original filepath 27 | self.filepath = filepath 28 | 29 | # drcov header attributes 30 | self.version = 0 31 | self.flavor = None 32 | 33 | # drcov module table 34 | self.module_table_count = 0 35 | self.module_table_version = 0 36 | self.modules = [] 37 | 38 | # drcov basic block data 39 | self.bb_table_count = 0 40 | self.bb_table_is_binary = True 41 | self.basic_blocks = [] 42 | 43 | # parse the given filepath 44 | self._parse_drcov_file(filepath) 45 | 46 | #-------------------------------------------------------------------------- 47 | # Public 48 | #-------------------------------------------------------------------------- 49 | 50 | def get_module(self, module_name, fuzzy=True): 51 | """ 52 | Get a module by its name. 53 | 54 | Note that this is a 'fuzzy' lookup by default. 55 | """ 56 | 57 | # fuzzy module name lookup 58 | if fuzzy: 59 | 60 | # attempt lookup using case-insensitive filename 61 | for module in self.modules: 62 | if module_name.lower() in module.filename.lower(): 63 | return module 64 | 65 | # 66 | # no hits yet... let's cleave the extension from the given module 67 | # name (if present) and try again 68 | # 69 | 70 | if "." in module_name: 71 | module_name = module_name.split(".")[0] 72 | 73 | # attempt lookup using case-insensitive filename without extension 74 | for module in self.modules: 75 | if module_name.lower() in module.filename.lower(): 76 | return module 77 | 78 | # strict lookup 79 | else: 80 | for module in self.modules: 81 | if module_name == module.filename: 82 | return module 83 | 84 | # no matching module exists 85 | return None 86 | 87 | def get_blocks_by_module(self, module_name): 88 | """ 89 | Extract coverage blocks pertaining to the named module. 90 | """ 91 | 92 | # locate the coverage that matches the given module_name 93 | module = self.get_module(module_name) 94 | 95 | # if we fail to find a module that matches the given name, bail 96 | if not module: 97 | raise ValueError("No coverage for module '%s' in log" % module_name) 98 | 99 | # extract module id for speed 100 | mod_id = module.id 101 | 102 | # loop through the coverage data and filter out data for only this module 103 | coverage_blocks = [bb.start for bb in self.basic_blocks if bb.mod_id == mod_id] 104 | 105 | # return the filtered coverage blocks 106 | return coverage_blocks 107 | 108 | #-------------------------------------------------------------------------- 109 | # Parsing Routines - Top Level 110 | #-------------------------------------------------------------------------- 111 | 112 | def _parse_drcov_file(self, filepath): 113 | """ 114 | Parse drcov coverage from the given log file. 115 | """ 116 | with open(filepath, "rb") as f: 117 | self._parse_drcov_header(f) 118 | self._parse_module_table(f) 119 | self._parse_bb_table(f) 120 | 121 | def _parse_drcov_data(self, drcov_data): 122 | """ 123 | Parse drcov coverage from the given data blob. 124 | """ 125 | pass # TODO/DRCOV 126 | 127 | #-------------------------------------------------------------------------- 128 | # Parsing Routines - Internals 129 | #-------------------------------------------------------------------------- 130 | 131 | def _parse_drcov_header(self, f): 132 | """ 133 | Parse drcov log header from filestream. 134 | """ 135 | 136 | # parse drcov version from log 137 | # eg: DRCOV VERSION: 2 138 | version_line = f.readline().strip() 139 | self.version = int(version_line.split(":")[1]) 140 | 141 | # parse drcov flavor from log 142 | # eg: DRCOV FLAVOR: drcov 143 | flavor_line = f.readline().strip() 144 | self.flavor = flavor_line.split(":")[1] 145 | 146 | assert self.version == 2, "Only drcov version 2 log files supported" 147 | 148 | def _parse_module_table(self, f): 149 | """ 150 | Parse drcov log module table from filestream. 151 | """ 152 | self._parse_module_table_header(f) 153 | self._parse_module_table_columns(f) 154 | self._parse_module_table_modules(f) 155 | 156 | def _parse_module_table_header(self, f): 157 | """ 158 | Parse drcov log module table header from filestream. 159 | 160 | ------------------------------------------------------------------- 161 | 162 | Format used in DynamoRIO v6.1.1 through 6.2.0 163 | eg: 'Module Table: 11' 164 | 165 | Format used in DynamoRIO v7.0.0-RC1 (and hopefully above) 166 | eg: 'Module Table: version X, count 11' 167 | 168 | """ 169 | 170 | # parse module table 'header' 171 | # eg: Module Table: version 2, count 11 172 | header_line = f.readline().strip() 173 | field_name, field_data = header_line.split(": ") 174 | #assert field_name == "Module Table" 175 | 176 | # 177 | # NOTE/COMPAT: 178 | # 179 | # DynamoRIO doesn't document their drcov log format, and it has 180 | # changed its format at least once during its lifetime. 181 | # 182 | # we just have to try parsing the table header one way to determine 183 | # if its the old (say, a 'v1') table, or the new 'v2' table. 184 | # 185 | 186 | try: 187 | 188 | # seperate 'version X' and 'count Y' from each other ('v2') 189 | version_data, count_data = field_data.split(", ") 190 | 191 | # failure to unpack indicates this is an 'older, v1' drcov log 192 | except ValueError: 193 | self.module_table_count = int(field_data) 194 | self.module_table_version = 1 195 | return 196 | 197 | # parse module table version out of 'version X' 198 | data_name, version = version_data.split(" ") 199 | #assert data_name == "version" 200 | self.module_table_version = int(version) 201 | if not self.module_table_version in [2, 3, 4]: 202 | raise ValueError("Unsupported (new?) drcov log format...") 203 | 204 | # parse module count in table from 'count Y' 205 | data_name, count = count_data.split(" ") 206 | #assert data_name == "count" 207 | self.module_table_count = int(count) 208 | 209 | def _parse_module_table_columns(self, f): 210 | """ 211 | Parse drcov log module table columns from filestream. 212 | 213 | ------------------------------------------------------------------- 214 | 215 | DynamoRIO v6.1.1, table version 1: 216 | eg: (Not present) 217 | 218 | DynamoRIO v7.0.0-RC1, table version 2: 219 | Windows: 220 | 'Columns: id, base, end, entry, checksum, timestamp, path' 221 | Mac/Linux: 222 | 'Columns: id, base, end, entry, path' 223 | 224 | DynamoRIO v7.0.17594B, table version 3: 225 | Windows: 226 | 'Columns: id, containing_id, start, end, entry, checksum, timestamp, path' 227 | Mac/Linux: 228 | 'Columns: id, containing_id, start, end, entry, path' 229 | 230 | DynamoRIO v7.0.17640, table version 4: 231 | Windows: 232 | 'Columns: id, containing_id, start, end, entry, offset, checksum, timestamp, path' 233 | Mac/Linux: 234 | 'Columns: id, containing_id, start, end, entry, offset, path' 235 | 236 | """ 237 | 238 | # NOTE/COMPAT: there is no 'Columns' line for the v1 table... 239 | if self.module_table_version == 1: 240 | return 241 | 242 | # parse module table 'columns' 243 | # eg: Columns: id, base, end, entry, checksum, timestamp, path 244 | column_line = f.readline().strip() 245 | field_name, field_data = column_line.split(": ") 246 | #assert field_name == "Columns" 247 | 248 | # seperate column names 249 | # Windows: id, base, end, entry, checksum, timestamp, path 250 | # Mac/Linux: id, base, end, entry, path 251 | columns = field_data.split(", ") 252 | 253 | def _parse_module_table_modules(self, f): 254 | """ 255 | Parse drcov log modules in the module table from filestream. 256 | """ 257 | 258 | # loop through each *expected* line in the module table and parse it 259 | for i in xrange(self.module_table_count): 260 | module = DrcovModule(f.readline().strip(), self.module_table_version) 261 | self.modules.append(module) 262 | 263 | def _parse_bb_table(self, f): 264 | """ 265 | Parse dcov log basic block table from filestream. 266 | """ 267 | self._parse_bb_table_header(f) 268 | self._parse_bb_table_entries(f) 269 | 270 | def _parse_bb_table_header(self, f): 271 | """ 272 | Parse drcov log basic block table header from filestream. 273 | """ 274 | 275 | # parse basic block table 'header' 276 | # eg: BB Table: 2792 bbs 277 | header_line = f.readline().strip() 278 | field_name, field_data = header_line.split(": ") 279 | #assert field_name == "BB Table" 280 | 281 | # parse basic block count out of 'X bbs' 282 | count_data, data_name = field_data.split(" ") 283 | #assert data_name == "bbs" 284 | self.bb_table_count = int(count_data) 285 | 286 | # peek at the next few bytes to determine if this is a binary bb table. 287 | # An ascii bb table will have the line: 'module id, start, size:' 288 | token = "module id" 289 | saved_position = f.tell() 290 | 291 | # is this an ascii table? 292 | if f.read(len(token)) == token: 293 | self.bb_table_is_binary = False 294 | 295 | # nope! binary table 296 | else: 297 | self.bb_table_is_binary = True 298 | 299 | # seek back to the start of the table 300 | f.seek(saved_position) 301 | 302 | def _parse_bb_table_entries(self, f): 303 | """ 304 | Parse drcov log basic block table entries from filestream. 305 | """ 306 | # allocate the ctypes structure array of basic blocks 307 | self.basic_blocks = (DrcovBasicBlock * self.bb_table_count)() 308 | 309 | if self.bb_table_is_binary: 310 | # read the basic block entries directly into the newly allocated array 311 | f.readinto(self.basic_blocks) 312 | 313 | else: # let's parse the text records 314 | text_entry = f.readline().strip() 315 | 316 | if text_entry != "module id, start, size:": 317 | raise ValueError("Invalid BB header: %r" % text_entry) 318 | 319 | pattern = re.compile(r"^module\[\s*(?P[0-9]+)\]\:\s*(?P0x[0-9a-f]+)\,\s*(?P[0-9]+)$") 320 | for basic_block in self.basic_blocks: 321 | text_entry = f.readline().strip() 322 | 323 | match = pattern.match(text_entry) 324 | if not match: 325 | raise ValueError("Invalid BB entry: %r" % text_entry) 326 | 327 | basic_block.start = int(match.group("start"), 16) 328 | basic_block.size = int(match.group("size"), 10) 329 | basic_block.mod_id = int(match.group("mod"), 10) 330 | 331 | #------------------------------------------------------------------------------ 332 | # drcov module parser 333 | #------------------------------------------------------------------------------ 334 | 335 | class DrcovModule(object): 336 | """ 337 | Parser & wrapper for module details as found in a drcov coverage log. 338 | 339 | A 'module' in this context is a .EXE, .DLL, ELF, MachO, etc. 340 | """ 341 | def __init__(self, module_data, version): 342 | self.id = 0 343 | self.base = 0 344 | self.end = 0 345 | self.size = 0 346 | self.entry = 0 347 | self.checksum = 0 348 | self.timestamp = 0 349 | self.path = "" 350 | self.filename = "" 351 | self.containing_id = 0 352 | 353 | # parse the module 354 | self._parse_module(module_data, version) 355 | 356 | @property 357 | def start(self): 358 | """ 359 | Compatability alias for the module base. 360 | 361 | DrCov table version 2 --> 3 changed this paramter name base --> start. 362 | """ 363 | return self.base 364 | 365 | def _parse_module(self, module_line, version): 366 | """ 367 | Parse a module table entry. 368 | """ 369 | data = module_line.split(", ") 370 | 371 | # NOTE/COMPAT 372 | if version == 1: 373 | self._parse_module_v1(data) 374 | elif version == 2: 375 | self._parse_module_v2(data) 376 | elif version == 3: 377 | self._parse_module_v3(data) 378 | elif version == 4: 379 | self._parse_module_v4(data) 380 | else: 381 | raise ValueError("Unknown module format (v%u)" % version) 382 | 383 | def _parse_module_v1(self, data): 384 | """ 385 | Parse a module table v1 entry. 386 | """ 387 | self.id = int(data[0]) 388 | self.size = int(data[1]) 389 | self.path = str(data[2]) 390 | self.filename = os.path.basename(self.path) 391 | 392 | def _parse_module_v2(self, data): 393 | """ 394 | Parse a module table v2 entry. 395 | """ 396 | self.id = int(data[0]) 397 | self.base = int(data[1], 16) 398 | self.end = int(data[2], 16) 399 | self.entry = int(data[3], 16) 400 | if len(data) == 7: # Windows Only 401 | self.checksum = int(data[4], 16) 402 | self.timestamp = int(data[5], 16) 403 | self.path = str(data[-1]) 404 | self.size = self.end-self.base 405 | self.filename = os.path.basename(self.path) 406 | 407 | def _parse_module_v3(self, data): 408 | """ 409 | Parse a module table v3 entry. 410 | """ 411 | self.id = int(data[0]) 412 | self.containing_id = int(data[1]) 413 | self.base = int(data[2], 16) 414 | self.end = int(data[3], 16) 415 | self.entry = int(data[4], 16) 416 | if len(data) == 7: # Windows Only 417 | self.checksum = int(data[5], 16) 418 | self.timestamp = int(data[6], 16) 419 | self.path = str(data[-1]) 420 | self.size = self.end-self.base 421 | self.filename = os.path.basename(self.path) 422 | 423 | def _parse_module_v4(self, data): 424 | """ 425 | Parse a module table v4 entry. 426 | """ 427 | self.id = int(data[0]) 428 | self.containing_id = int(data[1]) 429 | self.base = int(data[2], 16) 430 | self.end = int(data[3], 16) 431 | self.entry = int(data[4], 16) 432 | self.offset = int(data[5], 16) 433 | if len(data) == 7: # Windows Only 434 | self.checksum = int(data[6], 16) 435 | self.timestamp = int(data[7], 16) 436 | self.path = str(data[-1]) 437 | self.size = self.end-self.base 438 | self.filename = os.path.basename(self.path) 439 | 440 | #------------------------------------------------------------------------------ 441 | # drcov basic block parser 442 | #------------------------------------------------------------------------------ 443 | 444 | class DrcovBasicBlock(Structure): 445 | """ 446 | Parser & wrapper for basic block details as found in a drcov coverage log. 447 | 448 | NOTE: 449 | 450 | Based off the C structure as used by drcov - 451 | 452 | /* Data structure for the coverage info itself */ 453 | typedef struct _bb_entry_t { 454 | uint start; /* offset of bb start from the image base */ 455 | ushort size; 456 | ushort mod_id; 457 | } bb_entry_t; 458 | 459 | """ 460 | _pack_ = 1 461 | _fields_ = [ 462 | ('start', c_uint32), 463 | ('size', c_uint16), 464 | ('mod_id', c_uint16) 465 | ] 466 | 467 | #------------------------------------------------------------------------------ 468 | # Command Line Testing 469 | #------------------------------------------------------------------------------ 470 | 471 | if __name__ == "__main__": 472 | argc = len(sys.argv) 473 | argv = sys.argv 474 | 475 | # base usage 476 | if argc < 2: 477 | print("usage: {} ".format(os.path.basename(sys.argv[0]))) 478 | sys.exit() 479 | 480 | # attempt file parse 481 | x = DrcovData(argv[1]) 482 | for bb, _ in x.get_blocks_by_module("bo"): 483 | #for bb in x.basic_blocks: 484 | print("0x%08x" % bb) 485 | -------------------------------------------------------------------------------- /src/avd/helper/sources.py: -------------------------------------------------------------------------------- 1 | """ 2 | This File should contain known sources that might indicate an attacker controlled source. e.g. over network via read 3 | """ 4 | 5 | user_sources = [ 6 | "fgets", 7 | "tlMain", # Trustlets Symbol on for Kinabi Drivers 8 | ] -------------------------------------------------------------------------------- /src/avd/loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Loader shamelessly stolen from Niklaus Schiess deen ;-) https://github.com/takeshixx/deen 3 | """ 4 | 5 | import sys 6 | import inspect 7 | import pprint 8 | import pkgutil 9 | import importlib 10 | 11 | 12 | class PluginLoader(object): 13 | """ 14 | Used for Loading Plugins 15 | """ 16 | 17 | def __init__(self, argparser=None, base=None): 18 | """ 19 | Constructor to load plugins from the according directory. 20 | :param argparser: 21 | """ 22 | self._argparser = None 23 | self._subargparser = None 24 | self.plugins = [] 25 | self.base = base 26 | if argparser: 27 | self.argparser = argparser 28 | self.load_plugins() 29 | 30 | @property 31 | def argparser(self): 32 | return self._argparser 33 | 34 | @argparser.setter 35 | def argparser(self, argparser): 36 | self._argparser = argparser 37 | 38 | @property 39 | def available_plugins(self): 40 | """Returns a list of tuples of all available 41 | plugins in the plugin folder.""" 42 | return self.plugins 43 | 44 | def pprint_available_plugins(self): 45 | """Returns a pprint.pformat representation 46 | of all available plugins. It will most likely 47 | be a human readable list.""" 48 | pp = pprint.PrettyPrinter(indent=4) 49 | return pp.pformat([p[1].display_name for p in self.available_plugins]) 50 | 51 | def _get_plugin_classes_from_module(self, package): 52 | """An internal helper function that extracts 53 | all plugin classes from modules in the plugins 54 | folder.""" 55 | output = [] 56 | for m in self._get_submodules_from_namespace_package(package): 57 | module = importlib.import_module(m, package=None) 58 | for c in inspect.getmembers(module, inspect.isclass): 59 | # Only classes that start with Plugin will be loaded. 60 | if c[0].startswith('Plugin') and \ 61 | len(c[0].replace('Plugin', '')) != 0: 62 | # Call the prerequisites() function before loading plugin. 63 | # TODO implement at later stage for more commandline arguments 64 | if c[1].prerequisites(): 65 | # Check if the plugin wants to add additional CLI arguments. 66 | #if self.argparser: 67 | # if getattr(c[1], 'cmd_name', None) and c[1].cmd_name and \ 68 | # getattr(c[1], 'cmd_help', None) and c[1].cmd_help: 69 | # add_argparser_func = getattr(c[1], 'add_argparser', None) 70 | # if not self._subargparser and self._argparser: 71 | # self._subargparser = self._argparser.add_subparsers(dest='plugin_cmd') 72 | # add_argparser_func(self._subargparser, c[1].cmd_name, 73 | # c[1].cmd_help, c[1].aliases) 74 | output.append(c) 75 | #else: 76 | # LOGGER.warning('Prerequisits for plugin {} not met'.format(c[0])) 77 | else: 78 | return output 79 | 80 | 81 | def _get_submodules_from_namespace_package(self, package): 82 | """An internal helper function that returns 83 | a list of submodules in the given namespace 84 | package.""" 85 | output = [] 86 | for importer, module_name, _ in pkgutil.iter_modules(package.__path__, package.__name__ + '.'): 87 | output.append(module_name) 88 | return output 89 | 90 | def load_plugins(self): 91 | """ 92 | Load the available Plugins 93 | :return: 94 | """ 95 | generic = importlib.import_module('src.avd.plugins.generic') 96 | optional = importlib.import_module('src.avd.plugins.optional') 97 | special = importlib.import_module('src.avd.plugins.special') 98 | self.plugins = self._get_plugin_classes_from_module(generic) 99 | self.plugins += self._get_plugin_classes_from_module(optional) 100 | self.plugins += (self._get_plugin_classes_from_module(special)) 101 | 102 | 103 | def plugin_available(self, name): 104 | """Returns True if the given plugin name is available, 105 | False if not.""" 106 | return True if self.get_plugin(name) else False 107 | 108 | def get_plugin_instance(self, name): 109 | """Returns an instance of the plugin for the 110 | given name. This will most likely be the 111 | function that should be called in order to 112 | use the plugins.""" 113 | return self.get_plugin(name)() 114 | 115 | def get_plugin(self, name): 116 | """Returns the plugin module for the given name.""" 117 | for plugin in self.available_plugins: 118 | if name == plugin[0] or name == plugin[1].name or \ 119 | name == plugin[1].display_name or name in plugin[1].aliases: 120 | return plugin[1] 121 | else: 122 | return None 123 | 124 | def get_plugin_cmd_available(self, name): 125 | """Returns True if the given plugin cmd is available, 126 | False if not.""" 127 | return True if self.get_plugin_by_cmd_name(name) else False 128 | 129 | def get_plugin_cmd_name_instance(self, name): 130 | return self.get_plugin_by_cmd_name(name)() 131 | 132 | def get_plugin_by_cmd_name(self, name): 133 | """Returns the plugin module for the given cmd name.""" 134 | for plugin in self.available_plugins: 135 | if not getattr(plugin[1], 'cmd_name', None) or \ 136 | not plugin[1].cmd_name: 137 | continue 138 | if name == plugin[1].cmd_name or \ 139 | name in plugin[1].aliases: 140 | if getattr(plugin[1], 'process_cli', None): 141 | return plugin[1] 142 | else: 143 | return None 144 | 145 | def read_content_from_args(self): 146 | args = self.argparser.parse_args() 147 | content = None 148 | if getattr(args, 'infile', None) and args.infile: 149 | try: 150 | if args.infile == '-': 151 | try: 152 | stdin = sys.stdin.buffer 153 | except AttributeError: 154 | stdin = sys.stdin 155 | content = stdin.read() 156 | else: 157 | with open(args.infile, 'rb') as f: 158 | content = f.read() 159 | except KeyboardInterrupt: 160 | return 161 | elif getattr(args, 'data', None) and args.data: 162 | content = bytearray(args.data, 'utf8') 163 | return content -------------------------------------------------------------------------------- /src/avd/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Loader shamelessly stolen from Niklaus Schiess deen ;-) https://github.com/takeshixx/deen 3 | """ 4 | from binaryninja.binaryview import BinaryView 5 | import sys 6 | from collections import Counter 7 | from ..helper.binjaWrapper import get_basic_block_from_instr 8 | from src.avd.core.sliceEngine.slice import SliceEngine 9 | 10 | #from .bo import PluginBufferOverflow 11 | 12 | 13 | class Plugin(object): 14 | """The core plugin class that should be subclassed 15 | by every plugin. It provides some required 16 | class attributes that ease the process of writing 17 | new plugins.""" 18 | 19 | # In case an error happened, it should 20 | # be stored in this variable. 21 | error = None 22 | # Internal name for the plugin. 23 | name = '' 24 | # The name that will be displayed in the GUI. 25 | display_name = '' 26 | # A list of aliases for this plugin. Can 27 | # be empty if there is no aliases to the 28 | # plugin name. 29 | aliases = [] 30 | 31 | # List of vulnerabilities found. 32 | vulns = [] 33 | 34 | # Traces 35 | _traces = [] 36 | 37 | # BinaryView from BinaryNinja 38 | _binaryView = None 39 | 40 | # Arguments for better handling 41 | _args = None 42 | 43 | # Reference to the Slice Engine 44 | slice_engine = None 45 | 46 | def __init__(self, bv, args=None): 47 | self._binaryView = bv 48 | self._args = args 49 | self.slice_engine = SliceEngine(args, bv) 50 | 51 | def __del__(self): 52 | if len(self.vulns) > 0: 53 | for vuln in sorted(self.vulns, key=lambda x: x.probability, reverse=True): 54 | if len(self._traces) > 0: 55 | bb = get_basic_block_from_instr(self.bv, vuln.instr.address) 56 | vuln.cmd_print_finding(self._traces, bb) 57 | else: 58 | vuln.cmd_print_finding() 59 | 60 | @property 61 | def bv(self): 62 | return self._binaryView 63 | 64 | @bv.setter 65 | def bv(self, bv): 66 | self._binaryView = bv 67 | 68 | def set_traces(self, traces): 69 | self._traces = traces 70 | 71 | def append_vuln(self, v): 72 | """ 73 | This function prevents to have duplicate vulnerabilties 74 | :param Vulnerability v: 75 | :return None: 76 | """ 77 | if not [e_vuln for e_vuln in self.vulns if not e_vuln != v]: 78 | self.vulns.append(v) 79 | 80 | @staticmethod 81 | def prerequisites(): 82 | """A function that should return True if all 83 | prerequisites for this plugin are met or False 84 | if not. Here a plugin can e.g. check if the 85 | current Python version is suitable for the 86 | functionality or if required third party modules 87 | are installed.""" 88 | return True 89 | 90 | def run(self, bv): 91 | """ 92 | Every plugin must have a run method to execute the plugin and perform the analysis 93 | :param bv: 94 | :return: 95 | """ 96 | assert bv is not None 97 | assert isinstance(bv, BinaryView) 98 | 99 | 100 | @staticmethod 101 | def add_argparser(argparser, cmd_name, cmd_help, cmd_aliases=None): 102 | """This function allows plugins to add subcommands 103 | to argparse in order to be used via a seperate 104 | command/alias on the CLI. 105 | 106 | :param argparser: a ArgParser object 107 | :param cmd_name: a plugin's cmd_name class variable 108 | :param cmd_help: a plugin's cmd_help class variable 109 | :param cmd_aliases: a plugin's cmd_aliases class variable 110 | """ 111 | if not cmd_aliases: 112 | cmd_aliases = [] 113 | # Note: Python 2 argparse does not support aliases. 114 | if sys.version_info.major < 3 or \ 115 | (sys.version_info.major == 3 and 116 | sys.version_info.minor < 2): 117 | parser = argparser.add_parser(cmd_name, help=cmd_help) 118 | else: 119 | parser = argparser.add_parser(cmd_name, help=cmd_help, aliases=cmd_aliases) 120 | parser.add_argument('plugindata', action='store', 121 | help='input data', nargs='?') 122 | parser.add_argument('-r', '--revert', action='store_true', dest='revert', 123 | default=False, help='revert plugin process') 124 | parser.add_argument('-f', '--file', dest='plugininfile', default=None, 125 | help='file name or - for STDIN') 126 | 127 | def process_cli(self, args): 128 | # TODO Fix CLI parameters 129 | """Do whatever the CLI cmd should do. The args 130 | argument is the return of parse_args(). Must 131 | return the processed data. 132 | 133 | :param args: the output of argparse.parse_args() 134 | :return: the return of either process() or unprocess() 135 | """ 136 | if not self.content: 137 | if not args.plugindata: 138 | if not args.plugininfile: 139 | self.bv = self.read_content_from_file('-') 140 | else: 141 | self.bv = self.read_content_from_file(args.plugininfile) 142 | else: 143 | self.bv = args.plugindata 144 | if not self.bv: 145 | return 146 | return self.run(self.bv) 147 | 148 | 149 | def read_content_from_file(self, file): 150 | """If file is a filename, it will read and 151 | return it's content. If file is '-', read 152 | from STDIN instead of a file. 153 | 154 | :param file: filename of '-' for STDIN 155 | :return: content of filename or data from STDIN 156 | """ 157 | content = b'' 158 | try: 159 | if file == '-': 160 | try: 161 | stdin = sys.stdin.buffer 162 | except AttributeError: 163 | stdin = sys.stdin 164 | content = stdin.read() 165 | else: 166 | try: 167 | with open(file, 'rb') as f: 168 | content = f.read() 169 | except Exception as e: 170 | self.error = e 171 | except KeyboardInterrupt: 172 | return 173 | return content 174 | 175 | def write_to_stdout(self, data, nonewline=False): 176 | """Write processed data to STDOUT. It takes 177 | care of whether it's running in Python 2 or 3 178 | to properly write bytes to STDOUT. 179 | 180 | :param data: data to be written to STDOUT 181 | :param nonewline: if True, omit newline at the end 182 | """ 183 | try: 184 | # Python 3 185 | stdout = sys.stdout.buffer 186 | except AttributeError: 187 | # Python 2 188 | stdout = sys.stdout 189 | stdout.write(data) 190 | if not nonewline: 191 | stdout.write(b'\n') -------------------------------------------------------------------------------- /src/avd/plugins/generic/BufferOverflow.py: -------------------------------------------------------------------------------- 1 | from src.avd.plugins import Plugin 2 | from src.avd.reporter.vulnerability import Vulnerability 3 | from src.avd.helper import binjaWrapper, sources 4 | import re 5 | import collections 6 | import traceback 7 | from src.avd.core.sliceEngine.loopDetection import loop_analysis 8 | from binaryninja import MediumLevelILOperation, RegisterValueType, SSAVariable 9 | from sys import maxsize 10 | from tqdm import tqdm 11 | 12 | __all__ = ['PluginBufferOverflow'] 13 | 14 | 15 | class BoParams(object): 16 | def __init__(self, dst=None, src=None, n=None, format_ident=None): 17 | if dst is not None: 18 | self.dst = dst 19 | if src is not None: 20 | self.src = src 21 | if n is not None: 22 | self.n = n 23 | if format is not None: 24 | self.Format = format_ident 25 | 26 | 27 | class FakeRegister(object): 28 | def __init__(self, name, constant=False): 29 | self.name = name 30 | self.is_constant = constant 31 | 32 | 33 | def get_params(bv, addr): 34 | return binjaWrapper.get_medium_il_instruction(bv, addr.address).params 35 | 36 | 37 | 38 | def parse_format_string(s, params): 39 | string_match = re.findall(r'%[0-9]*[diuoxfegacspn]', s, re.I) 40 | format_vars = collections.OrderedDict() 41 | for i, form in enumerate(string_match): 42 | format_vars[form] = params[i] 43 | return format_vars 44 | 45 | 46 | def calc_size(var, func): 47 | if not var: 48 | return None 49 | if SSAVariable == type(var): 50 | var = var.var 51 | try: 52 | if len(func.stack_layout) - 1 == func.stack_layout.index(var): 53 | return abs(var.storage) 54 | else: 55 | return abs(var.storage) - abs(func.stack_layout[func.stack_layout.index(var) + 1].storage) 56 | except ValueError: 57 | # For some odd reason BN does screw up the stack layout.. bug? 58 | return 0 59 | 60 | 61 | # TODO Move to PrettyPrinter Class 62 | def print_f_call(arg): 63 | arg_iter = iter(arg[:]) 64 | fun_c = next(arg_iter) 65 | fun_c += "(" 66 | for i, ar in enumerate(arg_iter, 1): 67 | try: 68 | fun_c += ar 69 | except TypeError: 70 | if ar: 71 | fun_c += ar.name 72 | else: 73 | fun_c += "None" 74 | if i < (len(arg)-1): 75 | fun_c += ', ' 76 | fun_c += ");" 77 | return fun_c 78 | 79 | 80 | class PluginBufferOverflow(Plugin): 81 | name = "PluginBufferOverflow" 82 | display_name = "Buffer Overflow" 83 | cmd_name = "bo" 84 | cmd_help = "Search for Known Buffer Overflow patterns" 85 | 86 | def __init__(self, bv=None, args=None): 87 | super(PluginBufferOverflow, self).__init__(bv) 88 | self.arch_offsets = { 89 | 'armv7': 4, 90 | 'aarch64': 4, 91 | 'x86_64': 0, 92 | 'x86': 0, 93 | 'thumb2': 0, 94 | } 95 | self.bv = bv 96 | self.args = args 97 | self.bo_symbols = { 98 | "_memmove": BoParams(dst=0, src=1, n=2), 99 | "memmove": BoParams(dst=0, src=1, n=2), 100 | "_memcpy": BoParams(dst=0, src=1, n=2), 101 | "memcpy": BoParams(dst=0, src=1, n=2), 102 | "_strncpy": BoParams(dst=0, src=1, n=2), 103 | "strncpy": BoParams(dst=0, src=1, n=2), 104 | "_strcpy": BoParams(dst=0, src=1), 105 | "strcpy": BoParams(dst=0, src=1), 106 | "_strcat": BoParams(dst=0, src=1), 107 | "strcat": BoParams(dst=0, src=1), # TODO: strcat Needs to special checked if buffer was filled before! 108 | "_strncat": BoParams(dst=0, src=1, n=2), 109 | "strncat": BoParams(dst=0, src=1, n=2), 110 | "_sprintf": BoParams(dst=0, format_ident=1, src=2), 111 | "sprintf": BoParams(dst=0, format_ident=1, src=2), # TODO: Multiple Args & Calc Length of FormatString 112 | "_snprintf": BoParams(dst=0, src=3, n=1), 113 | "snprintf": BoParams(dst=0, src=3, n=1), 114 | "_vsprintf": BoParams(dst=0, src=2), 115 | "vsprintf": BoParams(dst=0, src=2), 116 | "_fgets": BoParams(dst=0, n=1), 117 | "fgets": BoParams(dst=0, n=1), 118 | "gets": BoParams(dst=0), 119 | "_gets": BoParams(dst=0), 120 | "__isoc99_scanf": BoParams(format_ident=0), 121 | } 122 | 123 | def set_bv(self, bv): 124 | self.bv = bv 125 | 126 | # TODO Add default Blacklist to avoid Parsing e.g. libc 127 | def deep_function_analysis(self): 128 | for func in tqdm(self.bv.functions, desc=self.name + " Deep Analysis", leave=False): 129 | func_mlil = func.medium_level_il 130 | for bb in tqdm(func_mlil, desc=self.name + " Deep: Basic Blocks in function", leave=False): 131 | for instr in bb: 132 | # MLIL Store might be interesting due to Compiler optimizations 133 | if instr.operation == MediumLevelILOperation.MLIL_STORE: 134 | # Check that Source is not Static # TODO might miss src > dest 135 | if instr.src.possible_values.type == RegisterValueType.UndeterminedValue: 136 | # Instruction should be in a loop. Otherwise a BoF is unlikely 137 | if loop_analysis(bb): 138 | # Slice to Source 139 | # TODO Currently only works for MLIL_STORE (e.g. ) 140 | src_visited_instr = self.slice_engine.do_backward_slice_with_variable( 141 | instr, 142 | func_mlil.ssa_form, 143 | instr.ssa_form.vars_read[1], 144 | list() 145 | ) 146 | dst_visited_instr = self.slice_engine.do_backward_slice_with_variable( 147 | instr, 148 | func_mlil.ssa_form, 149 | instr.ssa_form.vars_read[0], 150 | list() 151 | ) 152 | # TODO ugly hack. 153 | # Just take the last Sliced Variable (might fail when tracing functions backwards) 154 | # TODO This is just a hotfix when src or dst is None. 155 | # TODO problem by CWE121_Stack_Based_Buffer_Overflow__char_type_overrun_memcpy_01 156 | if not src_visited_instr or not dst_visited_instr: 157 | continue 158 | else: 159 | if len(dst_visited_instr[-1].instr.vars_read) == 0 or \ 160 | len(src_visited_instr[-1].instr.vars_read) == 0: 161 | # Architecture spezific where Instructions 162 | # can have direkt assignments to Registers 163 | # TODO Fix for partial assigments in ARM e.g [r4 + 0x11].b = (r6_1).b 164 | continue 165 | src = src_visited_instr[-1].instr.vars_read[0] 166 | dst = dst_visited_instr[-1].instr.vars_read[0] 167 | if SSAVariable == type(src): 168 | src = src.var 169 | if SSAVariable == type(dst): 170 | dst = dst.var 171 | src_size = calc_size(src, src.function) 172 | dst_size = calc_size(dst, dst.function) 173 | if src_size > dst_size: 174 | # Might be an overflow. Lets Check if Source comes from a nasty function. 175 | # pretty print array 176 | cf = list(["memcpy"]) 177 | cf.append(src) 178 | cf.append(dst) 179 | cf.append("") 180 | text = "{} 0x{:x}\t{}\n".format(func.name, 181 | instr.address, print_f_call(cf)) 182 | text += "\t\tPotential Overflow!\n" 183 | text += "\t\t\tdst {} = {}\n".format(dst.name, dst_size) 184 | text += "\t\t\tsrc {} = {}\n".format(src.name, src_size) 185 | v = Vulnerability("Potential Overflow", 186 | text, 187 | instr, 188 | "Deep Search found that the Source Size: {} appears to be " 189 | "bigger than the destination Size:" 190 | " {}".format(calc_size(src, func), calc_size(dst, func)), 191 | 50) 192 | if not func_mlil.get_var_uses(src): 193 | # Probably dealing with a reference. Currently not implemented in BN. 194 | # Hence.. parsing manually 195 | # TODO port it to a function 196 | #func_mlil = func_mlil.ssa_form 197 | func_mlil_ssa = func_mlil.ssa_form 198 | for n in self.slice_engine.get_manual_var_uses(func_mlil, src): 199 | if n not in src_visited_instr: 200 | if src in func_mlil[n].vars_read: 201 | for vs in func_mlil[n].vars_written: 202 | for ea in self.slice_engine.do_forward_slice(func_mlil[n], func_mlil_ssa): 203 | if func_mlil_ssa[ea].operation == MediumLevelILOperation.MLIL_CALL_SSA: 204 | if self.bv.get_function_at(func_mlil_ssa[ea].dest.constant).name in sources.user_sources: 205 | # Check wheter it is in known user input sources Increase Probability 206 | v.append_reason( 207 | "The Source Location was used by a known Source") 208 | v.probability = 80 209 | else: 210 | # TODO 211 | # Written 212 | pass 213 | self.append_vuln(v) 214 | 215 | @staticmethod 216 | def handle_single_destination(format_vars, ref, src_size, current_function): 217 | for f_str in format_vars: 218 | # TODO check if possible to delete 219 | size = 0 220 | # TODO Handle arch dependent max Size of int/double etc 221 | if "s" in f_str or "c" in f_str: 222 | try: 223 | # Adjustment for the format string 224 | size -= len(f_str) 225 | # TODO Calculate correct size with delimited size 226 | # if This fails there is prob no limitation 227 | size += int(f_str[1:-1]) 228 | except ValueError: 229 | """ 230 | Fall Through since there might be no Format Limitation. Simple calc source size 231 | """ 232 | size += src_size 233 | 234 | return size 235 | 236 | def handle_multi_destinations(self, format_vars, ref, current_function, cf): 237 | for f_str in format_vars: 238 | v = ref.function.get_stack_var_at_frame_offset(format_vars[f_str].offset, 239 | current_function.start) 240 | if "s" in f_str or "c" in f_str: 241 | buf = "" 242 | try: 243 | # if This fails there is prob no limitation 244 | size = int(f_str[1:-1]) 245 | except ValueError: 246 | """ 247 | Fall Through since there might be no Format Limitation. Might be unlimited User input 248 | """ 249 | size = maxsize 250 | dst_f_size = calc_size(v, current_function) 251 | if size >= dst_f_size: 252 | cf.append(v) 253 | overflow_size = "" if size - dst_f_size > maxsize / 2 else size - dst_f_size 254 | text = "{} 0x{:x}\t{}\n".format(ref.function.name, ref.address, print_f_call(cf)) 255 | text += "\t\tPotential Overflow!\n" 256 | text += "\t\t\tdst {} = {}\n".format(v.name, dst_f_size) 257 | text += "\t\t\tn = {}\n".format(overflow_size) 258 | instr = binjaWrapper.get_medium_il_instruction(self.bv, ref.address) 259 | v = Vulnerability("Potential Overflow", 260 | text, 261 | instr, 262 | "Format String might overflow Variable " 263 | "written to {} with size {} by {} Bytes".format(v.name, 264 | dst_f_size, 265 | overflow_size), 266 | 100) 267 | self.vulns.append(v) 268 | 269 | def run(self, bv=None, args=None): 270 | if bv is None: 271 | raise Exception("No state was provided by Binary Ninja. Something must be wrong") 272 | super(PluginBufferOverflow, self).__init__(bv, args) 273 | if args: 274 | if args.deep: 275 | self.deep_function_analysis() 276 | 277 | arch_offset = self.arch_offsets[self.bv.arch.name] 278 | for syms in tqdm(self.bo_symbols, desc=self.name, leave=False): 279 | symbol = self.bv.get_symbol_by_raw_name(syms) 280 | if symbol is not None: 281 | for ref in tqdm(self.bv.get_code_refs(symbol.address), 282 | desc=self.name + ": " + syms + " References", leave=False): 283 | current_function = ref.function 284 | addr = ref.address 285 | try: 286 | bo_src = self.bo_symbols.get(syms).src 287 | except AttributeError: 288 | bo_src = None 289 | # TODO granularer 290 | except: 291 | traceback.print_exc() 292 | 293 | try: 294 | bo_n = self.bo_symbols.get(syms).n 295 | except AttributeError: 296 | bo_n = None 297 | n = None 298 | # TODO granularer 299 | except: 300 | traceback.print_exc() 301 | 302 | try: 303 | bo_format = self.bo_symbols.get(syms).Format 304 | except AttributeError: 305 | bo_format = None 306 | # TODO granularer 307 | except: 308 | traceback.print_exc() 309 | 310 | try: 311 | bo_dst = self.bo_symbols.get(syms).dst 312 | except AttributeError: 313 | bo_dst = None 314 | # TODO granularer 315 | except: 316 | traceback.print_exc() 317 | 318 | cf = list([]) 319 | cf.append(syms) 320 | 321 | if bo_dst is not None: 322 | dst = current_function.get_parameter_at(addr, None, self.bo_symbols.get(syms).dst) 323 | if 'StackFrameOffset' not in str(dst.type): 324 | if hasattr(dst, "value"): 325 | dst_var = FakeRegister("") 326 | dst_size = dst.value 327 | elif 'UndeterminedValue' in str(dst.type): 328 | dst_var = FakeRegister("") 329 | dst_size = 0 330 | else: 331 | dst_var = FakeRegister(dst.reg) 332 | dst_size = maxsize 333 | else: 334 | dst_var = ref.function.get_stack_var_at_frame_offset(dst.offset + arch_offset, 335 | current_function.start) 336 | if dst_var is None: 337 | dst_var = ref.function.get_stack_var_at_frame_offset(dst.offset, current_function.start) 338 | dst_size = calc_size(dst_var, current_function) 339 | cf.append(dst_var) 340 | 341 | if bo_src is None and bo_n is None and bo_format is None: 342 | """ 343 | Handling unsafe uses of gets and fgets while only providing a single variable as destination 344 | Buffer 345 | """ 346 | text = "{} 0x{:x}\t{}\n".format(ref.function.name, addr, print_f_call(cf)) 347 | text += "\t\tPotential Overflow!\n" 348 | text += "\t\t\tdst {} = {}\n".format(dst_var.name, dst_size) 349 | instr = binjaWrapper.get_medium_il_instruction(self.bv, ref.address) 350 | v = Vulnerability("Potential Overflow", 351 | text, 352 | instr, 353 | "Uses of gets and fgets only passing the" 354 | " destination variable is highly critical", 355 | 100) 356 | self.vulns.append(v) 357 | continue 358 | 359 | if bo_src is not None: 360 | src = current_function.get_parameter_at(addr, None, self.bo_symbols.get(syms).src) 361 | if 'StackFrameOffset' not in str(src.type): 362 | if hasattr(src, "value"): 363 | src_var = FakeRegister("") 364 | src_size = src.value 365 | elif 'UndeterminedValue' in str(src.type): 366 | src_var = FakeRegister("") 367 | src_size = 0 368 | else: 369 | src_var = FakeRegister(src.reg) 370 | src_size = 0 371 | else: 372 | src_var = ref.function.get_stack_var_at_frame_offset(src.offset + arch_offset, 373 | current_function.start) 374 | if src_var is None: 375 | src_var = ref.function.get_stack_var_at_frame_offset(src.offset, current_function.start) 376 | src_size = calc_size(src_var, current_function) 377 | cf.append(src_var) 378 | 379 | if bo_n is not None: 380 | n = current_function.get_parameter_at(addr, None, self.bo_symbols.get(syms).n) 381 | if 'StackFrameOffset' not in str(n.type) and 'ConstantValue' not in str(n.type): 382 | try: 383 | if hasattr(n, "reg"): 384 | n = FakeRegister(n.reg, constant=n.reg.is_constant) 385 | else: 386 | tmp_instr = binjaWrapper.get_medium_il_instruction(bv, ref.address) 387 | n = tmp_instr.ssa_form.vars_read[self.bo_symbols.get(syms).n] 388 | n_val = "" 389 | # TODO delete 390 | # n = FakeRegister("", constant=n.is_constant) 391 | # TODO Fix Exception to be more precise 392 | except Exception as e: 393 | # TODO Fix tracebacks 394 | #traceback.print_exc() 395 | try: 396 | real_param_name = get_params(self.bv, ref)[bo_n].src.name 397 | n = FakeRegister(real_param_name) 398 | n_val = real_param_name 399 | except IndexError: 400 | # TODO binary ninja had a problem with correctly resolving the function parameters. 401 | # Need to try it manually 402 | continue 403 | except AttributeError: 404 | # Can happen on if instructions beeing referenced 405 | continue 406 | 407 | else: 408 | if n.is_constant: 409 | n_val = str(n.value) 410 | else: 411 | n_val = str(n) 412 | cf.append(n_val) 413 | 414 | # Print the function 415 | # print("{} 0x{:x}\t{}".format(ref.function.name, addr, print_f_call(cf))) 416 | # Handling Format Strings like scanf 417 | if bo_format is not None: 418 | params = [] 419 | for i in range(0, len(get_params(self.bv, ref))): 420 | params.append(current_function.get_parameter_at(addr, None, i)) 421 | format_string = binjaWrapper.get_constant_string(self.bv, 422 | params[self.bo_symbols.get(syms).Format].value) 423 | cf.insert(self.bo_symbols.get(syms).Format+1, "'" + format_string + "'") 424 | params.pop(bo_format) 425 | format_vars = parse_format_string(format_string, params) 426 | if bo_dst is not None: 427 | size = self.handle_single_destination(format_vars, ref, src_size, current_function) 428 | if not size: 429 | size = 0 430 | size += len(format_string) 431 | if size > dst_size: 432 | text = "{} 0x{:x}\t{}\n".format(ref.function.name, addr, print_f_call(cf)) 433 | text += "\t\tPotential Overflow!\n" 434 | text += "\t\t\tdst {} = {}\n".format(dst_var.name, dst_size) 435 | text += "\t\t\tsrc {} = {}\n".format(src_var.name, src_size) 436 | text += "\t\t\ttotal_length = {}\n".format(size) 437 | instr = binjaWrapper.get_medium_il_instruction(bv, ref.address) 438 | v = Vulnerability("Potential Overflow", 439 | text, 440 | instr, 441 | "Format function {} can overflow the " 442 | "destination Buffer with {} Bytes".format(syms, size - dst_size), 443 | 80) 444 | self.vulns.append(v) 445 | elif size == dst_size: 446 | # Check if the Format String ends the string properly 447 | last_format = list(format_vars.keys())[-1] 448 | ending_strings = ["\n", "\r", "\x00"] 449 | if not any(x in format_string[format_string.rfind(last_format) + len(last_format):] for x in 450 | ending_strings): 451 | text = "{} 0x{:x}\t{}\n".format(ref.function.name, addr, print_f_call(cf)) 452 | text += "\t\tPotential Overflow!\n" 453 | text += "\t\t\tdst {} = {}\n".format(dst_var.name, dst_size) 454 | instr = binjaWrapper.get_medium_il_instruction(bv, ref.address) 455 | v = Vulnerability("Potential Overflow", 456 | text, 457 | instr, 458 | "The source and destination size are equal. " 459 | "There might be no Nullbyte/String delimiter", 460 | 60) 461 | self.vulns.append(v) 462 | else: 463 | self.handle_multi_destinations(format_vars, ref, current_function, cf) 464 | continue 465 | 466 | if bo_src is not None and bo_n is None and bo_dst is not None: 467 | if src_size > dst_size: 468 | """ 469 | Source Size is Bigger than dst_size 470 | """ 471 | text = "{} 0x{:x}\t{}\n".format(ref.function.name, addr, print_f_call(cf)) 472 | text += "\t\tPotential Overflow!\n" 473 | text += "\t\t\tdst {} = {}\n".format(dst_var.name, dst_size) 474 | text += "\t\t\tsrc {} = {}\n".format(src_var.name, src_size) 475 | instr = binjaWrapper.get_medium_il_instruction(bv, ref.address) 476 | v = Vulnerability("Potential Overflow", 477 | text, 478 | instr, 479 | "The Source Buffer Size is bigger than the destination Buffer", 80) 480 | self.vulns.append(v) 481 | continue 482 | elif bo_src is not None and bo_n is not None: 483 | if SSAVariable == type(n): 484 | """ 485 | N Value is undermined and source is not known. This will trigger a reverse Slice 486 | to find the initiating part and check whether it might be attacker controlled against 487 | the sources array 488 | """ 489 | # Follow N 490 | instr = binjaWrapper.get_medium_il_instruction(bv, ref.address) 491 | slice_sources = self.slice_engine.get_sources2(bv, instr, n) 492 | intersection_slices = [x for x in slice_sources if x in sources.user_sources] 493 | if intersection_slices: 494 | text = "{} 0x{:x}\t{}\n".format(ref.function.name, addr, print_f_call(cf)) 495 | text += "\t\tPotential Overflow!\n" 496 | text += "\t\t\tdst {} = {}\n".format(dst_var.name, dst_size) 497 | text += "\t\t\tsrc {} = {}\n".format(src_var.name, src_size) 498 | instr = binjaWrapper.get_medium_il_instruction(bv, ref.address) 499 | v = Vulnerability("Potential Overflow", 500 | text, 501 | instr, 502 | "The amount of bytes copied might be user controlled through the " 503 | "following sources {}\nFull Trace of functions for N is {}".format( 504 | intersection_slices, slice_sources), 60) 505 | 506 | if src_size > dst_size: 507 | v.probability = 90 508 | v.append_reason("The source buffer is also bigger than the destination Buffer") 509 | self.vulns.append(v) 510 | continue 511 | if hasattr(n, "is_constant"): 512 | if bo_n is not None and n.is_constant: 513 | # N is constant 514 | if n.value > dst_size: 515 | """ 516 | N Value is bigger than dst size. 517 | """ 518 | text = "{} 0x{:x}\t{}\n".format(ref.function.name, addr, print_f_call(cf)) 519 | text += "\t\tPotential Overflow!\n" 520 | text += "\t\t\tdst {} = {}\n".format(dst_var.name, dst_size) 521 | text += "\t\t\tn {} = {}\n".format(str(n), n_val) 522 | instr = binjaWrapper.get_medium_il_instruction(bv, ref.address) 523 | v = Vulnerability("Potential Overflow", 524 | text, 525 | instr, 526 | "The amount of Copied bytes is bigger than the destination Buffer", 527 | 100) 528 | self.vulns.append(v) 529 | elif n.value == dst_size: 530 | """ 531 | Might indicate a Off-By-One 532 | """ 533 | # TODO 534 | pass 535 | if bo_src is not None: 536 | if hasattr(src_var, "name") and hasattr(n, "name"): 537 | if src_var.name == "" and n.name == "": 538 | pass 539 | 540 | -------------------------------------------------------------------------------- /src/avd/plugins/generic/IntegerOverflow.py: -------------------------------------------------------------------------------- 1 | from src.avd.plugins import Plugin 2 | from tqdm import tqdm 3 | from binaryninja import SSAVariable, MediumLevelILOperation 4 | from src.avd.reporter.vulnerability import Vulnerability 5 | from src.avd.helper import sources 6 | from z3 import * 7 | 8 | __all__ = ['PluginIntegerOverflow'] 9 | 10 | 11 | class PluginIntegerOverflow(Plugin): 12 | name = "PluginIntegerOverflow" 13 | display_name = "Integer Overflow" 14 | cmd_name = "io" 15 | cmd_help = "Search for Integer Overflows." 16 | 17 | _value_ops = [ 18 | MediumLevelILOperation.MLIL_ADD, 19 | MediumLevelILOperation.MLIL_ADC, 20 | MediumLevelILOperation.MLIL_DIVS, 21 | MediumLevelILOperation.MLIL_MUL, 22 | MediumLevelILOperation.MLIL_SUB 23 | ] 24 | 25 | def __init__(self, bv=None): 26 | super(PluginIntegerOverflow, self).__init__(bv) 27 | self.bv = bv 28 | 29 | def set_bv(self, bv): 30 | self.bv = bv 31 | 32 | def run(self, bv=None, args=None, traces=None): 33 | super(PluginIntegerOverflow, self).__init__(bv) 34 | self._find_int_overflow() 35 | return 36 | 37 | def _check_maybe_constrolled(self, instr, var): 38 | slice_sources = self.slice_engine.get_sources2(self.bv, instr, var) 39 | return [x for x in slice_sources if x in sources.user_sources] 40 | 41 | def _check_if_value_operation(self, instr): 42 | for ops in instr.operands: 43 | if hasattr(ops, "operation"): 44 | if ops.operation in self._value_ops: 45 | right_ops_sources = self._check_maybe_constrolled(instr, ops.right.src) 46 | left_ops_sources = self._check_maybe_constrolled(instr, ops.left.src) 47 | if len(right_ops_sources) or len(left_ops_sources): 48 | # Stupid Approach at first 49 | # Part of the Instruction can be attacker Controlled. 50 | text = "MLIL {} 0x{:x}\n".format(instr.function.source_function.name, instr.address) 51 | text += "\t\tPotential Integer Overflow\n\t\tInstruction: {}\n".format(instr.non_ssa_form) 52 | if right_ops_sources: 53 | text += "\t\t\t Variable {} might be attacker controlled with {}\n".format( 54 | ops.right.src, str(right_ops_sources)) 55 | if left_ops_sources: 56 | text += "\t\t\t Variable {} might be attacker controlled with {}\n".format( 57 | ops.left.src, str(left_ops_sources)) 58 | 59 | vuln = Vulnerability("Potential Integer Overflow problem!", 60 | text, 61 | instr, 62 | "It appears that parts of the calculation can be attacker controlled", 63 | 60) 64 | self.vulns.append(vuln) 65 | # TODO Perform z3 Analysis to check for multiple problems 66 | 67 | 68 | # https://github.com/0vercl0k/z3-playground/blob/master/proof_unsigned_integer_overflow_chech.py 69 | #ops.possible_values 70 | #left_var = BitVecs('left_var', ops.left.src.width * 8) 71 | #right_var = BitVecs('right_var', ops.right.src.width * 8) 72 | 73 | def _find_int_overflow(self): 74 | for func in tqdm(self.bv.functions, desc=self.name, leave=False): 75 | # Only check one function 76 | if not func.start == 0x16ab: 77 | continue 78 | for bb in func.medium_level_il.ssa_form: 79 | for instr in bb: 80 | if not instr.instr_index == 41: 81 | continue 82 | # Usually int operations are at the very beginning SET_VAR operations 83 | if instr.operation == MediumLevelILOperation.MLIL_SET_VAR_SSA: 84 | if self._check_if_value_operation(instr): 85 | print(instr) 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/avd/plugins/generic/OutOfBounds.py: -------------------------------------------------------------------------------- 1 | from src.avd.plugins import Plugin 2 | #from ..reporter.vulnerability import Vulnerability 3 | #from ..helper import binjaWrapper, sources 4 | #import re 5 | #import collections 6 | #import traceback 7 | #from src.avd.core.sliceEngine import slice 8 | from src.avd.core.sliceEngine.loopDetection import loop_analysis 9 | #from binaryninja import MediumLevelILOperation, RegisterValueType, SSAVariable 10 | #from sys import maxsize 11 | 12 | __all__ = ['PluginOutOfBounds'] 13 | 14 | 15 | class PluginOutOfBounds(Plugin): 16 | name = "PluginOutOfBounds" 17 | display_name = "Out of Bounds" 18 | cmd_name = "oob" 19 | cmd_help = "Search for Out of Bounds Access" 20 | 21 | def __init__(self, bv=None): 22 | super(PluginOutOfBounds, self).__init__(bv) 23 | self.bv = bv 24 | 25 | def set_bv(self, bv): 26 | self.bv = bv 27 | 28 | def run(self, bv=None, args=None, traces=None): 29 | super(PluginOutOfBounds, self).__init__(bv) 30 | #self.find_possible_arrays() 31 | return 32 | 33 | def find_possible_arrays(self): 34 | """ 35 | Searches for possible array declerations on mlil. Later SSA will be used to triage 36 | :return: 37 | """ 38 | array_def_functions = ["MLIL_SET_VAR", "MLIL_CONST_PTR"] 39 | for func in self.bv.functions: 40 | func_ssa = func.medium_level_il 41 | for bb in func_ssa: 42 | for instr in bb: 43 | if instr.operation.name in array_def_functions: 44 | #for bb in self.bv.get_basic_blocks_at(instr.address): 45 | #if loop_analysis(bb): 46 | # TODO find whether the var is used either in a loop or access directly 47 | # https://github.com/cetfor/PaperMachete/blob/master/queries/cwe_788_v1.py 48 | pass 49 | #print(instr) 50 | -------------------------------------------------------------------------------- /src/avd/plugins/generic/SignedAnalysis.py: -------------------------------------------------------------------------------- 1 | from src.avd.plugins import Plugin 2 | from src.avd.reporter.vulnerability import Vulnerability 3 | from binaryninja import MediumLevelILOperation 4 | from tqdm import tqdm 5 | 6 | 7 | class PluginSignedAnalysis(Plugin): 8 | name = "SignedAnalysis" 9 | display_name = "SignedAnalysis" 10 | cmd_name = "SignedAnalysis" 11 | cmd_help = "Find problems with signed/unsinged numbers" 12 | 13 | # It would be awesome to keep updating this list 14 | unsigned_sinks = dict( 15 | malloc=0, 16 | memcpy=2, 17 | read=2, 18 | pread=2, 19 | memmove=2, 20 | strncpy=2 21 | ) 22 | 23 | def __init__(self, bv=None): 24 | """ 25 | Constructor 26 | :param bv: 27 | """ 28 | super(PluginSignedAnalysis, self).__init__(bv) 29 | self.bv = bv 30 | 31 | def set_bv(self, bv): 32 | """ 33 | Settter for the BinaryView 34 | :param bv: 35 | :return: 36 | """ 37 | self.bv = bv 38 | 39 | def run(self, bv=None, args=None, traces=None): 40 | """ 41 | Run Function (Required) 42 | :param bv: 43 | :param args: 44 | :param traces: 45 | :return: 46 | """ 47 | super(PluginSignedAnalysis, self).__init__(bv) 48 | for funcs in tqdm(self.bv.functions, desc=self.name, leave=False): 49 | self._function_sign_analysis_start(funcs) 50 | return 51 | 52 | def _function_sign_analysis_start(self, func): 53 | """ 54 | Finding conversions problems to size_t sinks 55 | :param func: 56 | :return: 57 | """ 58 | for blocks in func.medium_level_il: 59 | for instr in blocks: 60 | if instr.operation == MediumLevelILOperation.MLIL_CALL: 61 | try: 62 | call_name = self.bv.get_function_at(instr.dest.constant).name 63 | except AttributeError: 64 | # Bypass GOT references... 65 | continue 66 | 67 | if call_name in self.unsigned_sinks.keys(): 68 | try: 69 | if instr.vars_read[self.unsigned_sinks.get(call_name)].type.signed: 70 | text = "MLIL {} 0x{:x}\n".format(func.name, instr.address) 71 | text += "\t\tPotential bad sign conversion\n" 72 | text += "\t\t\tVariable {} is signed but will be implicitly" \ 73 | " converted by {} to size_t\n".format( 74 | instr.vars_read[self.unsigned_sinks.get(call_name)], 75 | call_name 76 | ) 77 | 78 | vuln = Vulnerability("Potential signedness problem!", 79 | text, 80 | instr, 81 | "It appears that signed variable is converted by an implicit " 82 | "conversion with a function call.", 83 | 60) 84 | self.vulns.append(vuln) 85 | 86 | except IndexError: 87 | # Sometimes there are static values to the function call... bypassing it 88 | continue 89 | -------------------------------------------------------------------------------- /src/avd/plugins/generic/UninitializedVariable.py: -------------------------------------------------------------------------------- 1 | from src.avd.plugins import Plugin 2 | from src.avd.reporter.vulnerability import Vulnerability 3 | from binaryninja import MediumLevelILOperation, VariableSourceType 4 | from src.avd.core.sliceEngine.loopDetection import graph_function 5 | from tqdm import tqdm 6 | import concurrent 7 | import concurrent.futures 8 | 9 | __all__ = ['PluginUninitializedVariable'] 10 | 11 | 12 | class PluginUninitializedVariable(Plugin): 13 | name = "UninitializedVariable" 14 | display_name = "UninitializedVariable" 15 | cmd_name = "UninitializedVariable" 16 | cmd_help = "Search for Uninitialized Variables" 17 | 18 | # This Dict will whitelist some functions where variables are initialized 19 | KnownFunctions = {"__isoc99_sscanf": 1, 20 | "__isoc99_swscanf": 1, 21 | "pthread_create": 0} 22 | 23 | def __init__(self, bv=None): 24 | """ 25 | Constructor 26 | :param bv: 27 | """ 28 | super(PluginUninitializedVariable, self).__init__(bv) 29 | self.bv = bv 30 | self._threshold = None 31 | 32 | def set_bv(self, bv): 33 | """ 34 | Settter for the BinaryView 35 | :param bv: 36 | :return: 37 | """ 38 | self.bv = bv 39 | 40 | def run(self, bv=None, args=None, traces=None): 41 | """ 42 | Run Function (Required) 43 | :param bv: 44 | :param args: 45 | :param traces: 46 | :return: 47 | """ 48 | super(PluginUninitializedVariable, self).__init__(bv) 49 | if not args.deep: 50 | self._threshold = 20000 51 | if args.fast: 52 | self._threshold = 1000 53 | self._find_uninitialized_variables() 54 | return 55 | 56 | def check_whitelist(self, mlil_func, occur): 57 | """ 58 | Whitelisting some functions like scanf. Since they take a variable as parameter and then initializing it. 59 | :param mlil_func: 60 | :param occur: 61 | :return: 62 | """ 63 | for ea in self.slice_engine.do_forward_slice_with_variable(mlil_func[occur], mlil_func.ssa_form): 64 | if mlil_func.ssa_form[ea[0]].operation == MediumLevelILOperation.MLIL_CALL_SSA: 65 | if self.bv.get_function_at( 66 | mlil_func.ssa_form[ea[0]].dest.constant).name in self.KnownFunctions.keys(): 67 | vars = mlil_func.ssa_form[ea[0]].vars_read 68 | if ea[1] in vars: 69 | if vars.index(ea[1]) == self.KnownFunctions[self.bv.get_function_at(mlil_func.ssa_form[ea[0]].dest.constant).name]: 70 | return True 71 | else: 72 | return False 73 | return True 74 | return False 75 | 76 | def check_occurance(self, mlil_func, custom_bb, v): 77 | """ 78 | Check if variable is present in custom given basic blocks 79 | :param mlil_func: 80 | :param custom_bb: 81 | :param v: 82 | :return: 83 | """ 84 | for occur in self.slice_engine.get_manual_var_uses_custom_bb(custom_bb, v): 85 | if v in mlil_func[occur].vars_written: 86 | break 87 | # Return addr usually not written. Bypass it against false positives 88 | if "__return_addr" == v.name: 89 | break 90 | # Filter out Edge cases like Scanff 91 | if not self.check_whitelist(mlil_func, occur): 92 | return True 93 | else: 94 | return False 95 | return False 96 | 97 | def _find_all_paths(self, graph, start_vertex, end_vertex, path=[]): 98 | """ 99 | This function computes all possible paths inside the given graph (function) performing DFS 100 | :param graph: 101 | :param start_vertex: 102 | :param end_vertex: 103 | :param path: 104 | :return: 105 | """ 106 | path = path + [start_vertex] 107 | if start_vertex == end_vertex: 108 | return [path] 109 | if not graph.vertices.has_key(start_vertex): 110 | return [] 111 | paths = [] 112 | for node in graph.get_vertex_from_index(start_vertex).get_successor_indices(): 113 | if node not in path: 114 | newpaths = self._find_all_paths(graph, node, end_vertex, path) 115 | for newpath in newpaths: 116 | paths.append(newpath) 117 | return paths 118 | 119 | # Create a Control Flow graph of basic blocks inside a function 120 | def _create_function_control_flow(self, func): 121 | """ 122 | This function creates the inital control flow graph from a function and computes all paths inside of it 123 | :param func: 124 | :return: 125 | """ 126 | # Create Graph 127 | g = graph_function(func) 128 | if self._threshold: 129 | g.set_threshold(self._threshold) 130 | return g.compute_all_paths() 131 | 132 | def _is_in_vulns(self, instr): 133 | """ 134 | Check if the instruction is already present in current vulnerabilities (preventing duplicates) 135 | :param instr: 136 | :return: 137 | """ 138 | for tmp_vuln in self.vulns: 139 | if tmp_vuln.instr.address == instr.address: 140 | return True 141 | 142 | return False 143 | 144 | def _map_funcs(self, funcs): 145 | """ 146 | Parallized function to map single functions 147 | :param funcs: 148 | :return: 149 | """ 150 | control_flow = self._create_function_control_flow(funcs) 151 | mlil_func = funcs.medium_level_il 152 | for var in tqdm(funcs.stack_layout, desc=self.name + " " + funcs.name, leave=False): 153 | instr_index_array = mlil_func.get_var_uses(var) 154 | if len(instr_index_array) > 0: 155 | for mlil_instr_index in instr_index_array: 156 | mlil_instr = mlil_func[mlil_instr_index] 157 | for v in mlil_instr.vars_read: 158 | if v.source_type == VariableSourceType.StackVariableSourceType: 159 | for bb_path in control_flow: 160 | if self.check_occurance(mlil_func, bb_path, v): 161 | instr = mlil_instr 162 | if self._is_in_vulns(instr): 163 | break 164 | 165 | text = "MLIL {} 0x{:x}\n".format(funcs.name, mlil_instr.address) 166 | text += "\t\tPotential use of uninitialized variable!\n" 167 | text += "\t\t\tVariable {}\n".format(v.name) 168 | text += "\n\t\tThe following basic block path (MLIL) was taken without " \ 169 | "initializing the variable\n" 170 | for bb in bb_path: 171 | text += "\t\t\t{}\n".format(bb) 172 | 173 | vuln = Vulnerability("Potential use of uninitialized variable!", 174 | text, 175 | instr, 176 | "A Variable on the Stack appears to be used before initialized.", 177 | 60) 178 | self.vulns.append(vuln) 179 | else: 180 | continue 181 | 182 | def _find_uninitialized_variables(self): 183 | """ 184 | Main Function to find uninitialized variables 185 | :return: 186 | """ 187 | with concurrent.futures.ThreadPoolExecutor(max_workers=40) as executor: 188 | [executor.submit(self._map_funcs, func) for func in self.bv.functions] 189 | 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /src/avd/plugins/optional/LargeStackFrame.py: -------------------------------------------------------------------------------- 1 | from src.avd.plugins import Plugin 2 | from tqdm import tqdm 3 | from binaryninja import SSAVariable 4 | from src.avd.reporter.vulnerability import Vulnerability 5 | 6 | __all__ = ['PluginLargeStackFrame'] 7 | 8 | 9 | class PluginLargeStackFrame(Plugin): 10 | name = "PluginLargeStackFrame" 11 | display_name = "Large Stack Frame" 12 | cmd_name = "lsf" 13 | cmd_help = "Search for large stack buffers." 14 | 15 | # TODO make it dynamic with arguments 16 | _threshold = 150 17 | 18 | def __init__(self, bv=None): 19 | super(PluginLargeStackFrame, self).__init__(bv) 20 | self.bv = bv 21 | 22 | def set_bv(self, bv): 23 | self.bv = bv 24 | 25 | def run(self, bv=None, deep=None, traces=None): 26 | super(PluginLargeStackFrame, self).__init__(bv) 27 | self._find_large_stack_frames() 28 | return 29 | 30 | @staticmethod 31 | def _calc_size(var, func): 32 | if SSAVariable == type(var): 33 | var = var.var 34 | 35 | if len(func.stack_layout) - 1 == func.stack_layout.index(var): 36 | return abs(var.storage) 37 | else: 38 | return abs(var.storage) - abs(func.stack_layout[func.stack_layout.index(var) + 1].storage) 39 | 40 | def _find_large_stack_frames(self): 41 | for func in tqdm(self.bv.functions, desc=self.name, leave=False): 42 | for var in func.stack_layout: 43 | size = self._calc_size(var, func) 44 | if size >= self._threshold: 45 | text = "{} 0x{:x}\n".format(func.name, func.start) 46 | text += "\t\tFound Large Stack Variable: {0} with size {1} in function {2} at address {3}".format( 47 | var.name, size, var.function.name, hex(var.function.start)) 48 | 49 | vuln = Vulnerability("Large Stack Frame!", 50 | text, 51 | None, 52 | "Large Stack Frames can be an indicator of potential misuse.", 53 | 30) 54 | self.vulns.append(vuln) 55 | -------------------------------------------------------------------------------- /src/avd/plugins/special/FindHeartbleed.py: -------------------------------------------------------------------------------- 1 | import math 2 | from src.avd.plugins import Plugin 3 | 4 | from binaryninja import (BinaryViewType, MediumLevelILInstruction, 5 | MediumLevelILOperation, RegisterValueType, 6 | SSAVariable) 7 | from tqdm import tqdm 8 | from z3 import (UGT, ULT, And, Array, BitVec, BitVecSort, Concat, Extract, 9 | LShR, Not, Or, Solver, ZeroExt, simplify, unsat) 10 | 11 | from src.avd.reporter.vulnerability import Vulnerability 12 | from functools import reduce 13 | from src.avd.helper import binjaWrapper 14 | 15 | # idea: assume byte swapping means that there will be 2+ assignments 16 | # that must be a single byte. Each time an MLIL_SET_VAR_SSA operation 17 | # is encountered, we can check if the value of that operation is constrained 18 | # to 0 <= x <= 0xff. 19 | 20 | class BNILVisitor(object): 21 | def __init__(self, **kw): 22 | super(BNILVisitor, self).__init__() 23 | 24 | def visit(self, expression): 25 | method_name = 'visit_{}'.format(expression.operation.name) 26 | if hasattr(self, method_name): 27 | value = getattr(self, method_name)(expression) 28 | else: 29 | value = None 30 | return value 31 | 32 | 33 | def create_BitVec(ssa_var, size): 34 | return BitVec( 35 | '{}#{}'.format( 36 | ssa_var.var.name, ssa_var.version 37 | ), 38 | size * 8 if size else 1 39 | ) 40 | 41 | 42 | def identify_byte(var, function): 43 | if isinstance(var, SSAVariable): 44 | possible_values = function[1].get_ssa_var_possible_values(var) 45 | try: 46 | size = function[function.get_ssa_var_definition(var)].size 47 | except AttributeError: 48 | return None 49 | except IndexError: 50 | return None 51 | except TypeError: 52 | return None 53 | else: 54 | possible_values = var.possible_values 55 | size = var.size 56 | 57 | if (possible_values.type == RegisterValueType.UnsignedRangeValue and 58 | len(possible_values.ranges) == 1): 59 | value_range = possible_values.ranges[0] 60 | start = value_range.start 61 | end = value_range.end 62 | step = value_range.step 63 | 64 | for i in range(size): 65 | if (start, end, step) == (0, (0xff << (8 * i)), (1 << (8 * i))): 66 | return value_range 67 | 68 | 69 | class ModelIsConstrained(Exception): 70 | pass 71 | 72 | 73 | class ByteSwapModeler(BNILVisitor): 74 | def __init__(self, var, address_size): 75 | super(ByteSwapModeler, self).__init__() 76 | 77 | if (not isinstance(var, MediumLevelILInstruction) or 78 | var.operation != MediumLevelILOperation.MLIL_VAR_SSA): 79 | raise TypeError('var must be an MLIL_VAR_SSA operation') 80 | 81 | self.address_size = address_size 82 | self._memory = Array( 83 | 'Memory', 84 | BitVecSort(address_size*8), 85 | BitVecSort(8) 86 | ) 87 | self.solver = Solver() 88 | self.visited = set() 89 | self.to_visit = list() 90 | self.byte_values = dict() 91 | self.byte_vars = set() 92 | self.var = var 93 | self.function = var.function 94 | 95 | def model_variable(self): 96 | var_def = self.function.get_ssa_var_definition(self.var.src) 97 | 98 | # Visit statements that our variable directly depends on 99 | self.to_visit.append(var_def) 100 | 101 | while self.to_visit: 102 | idx = self.to_visit.pop() 103 | if idx is not None: 104 | self.visit(self.function[idx]) 105 | 106 | # See if any constraints on the memcpy are directly influenced by 107 | # the variables that we know should be single bytes. This means 108 | # they likely constrain a potential byte swap. 109 | for i, branch in self.var.branch_dependence.items(): 110 | for vr in self.function[i].vars_read: 111 | if vr in self.byte_vars: 112 | raise ModelIsConstrained() 113 | vr_def = self.function.get_ssa_var_definition(vr) 114 | if vr_def is None: 115 | continue 116 | for vr_vr in self.function[vr_def].vars_read: 117 | if vr_vr in self.byte_vars: 118 | pass 119 | # TODO raise ModelIsConstrained 120 | 121 | def is_byte_swap(self): 122 | try: 123 | self.model_variable() 124 | except ModelIsConstrained: 125 | return False 126 | 127 | # Figure out if this might be a byte swap 128 | byte_values_len = len(self.byte_values) 129 | if 1 < byte_values_len <= self.var.src.var.type.width: 130 | var = create_BitVec(self.var.src, self.var.src.var.type.width) 131 | 132 | ordering = list(reversed([ 133 | self.byte_values[x] 134 | for x in sorted(self.byte_values.keys()) 135 | ])) 136 | 137 | reverse_var = Concat( 138 | *reversed([ 139 | Extract(i-1, i-8, var) 140 | for i in range(len(ordering) * 8, 0, -8) 141 | ]) 142 | ) 143 | 144 | if len(ordering) < 4: 145 | reverse_var = Concat( 146 | Extract( 147 | 31, 148 | len(ordering)*8, var 149 | ), 150 | reverse_var 151 | ) 152 | 153 | reversed_ordering = reversed(ordering) 154 | reversed_ordering = Concat(*reversed_ordering) 155 | 156 | # The idea here is that if we add the negation of this, if it's 157 | # not satisfiable, then that means there is no value such that 158 | # the equivalence does not hold. If that is the case, then this 159 | # should be a byte-swapped value. 160 | self.solver.add( 161 | Not( 162 | And( 163 | var == ZeroExt( 164 | var.size() - len(ordering)*8, 165 | Concat(*ordering) 166 | ), 167 | reverse_var == ZeroExt( 168 | reverse_var.size() - reversed_ordering.size(), 169 | reversed_ordering 170 | ) 171 | ) 172 | ) 173 | ) 174 | 175 | if self.solver.check() == unsat: 176 | return True 177 | 178 | return False 179 | 180 | def visit_MLIL_SET_VAR_SSA(self, expr): 181 | dest = create_BitVec(expr.dest, expr.size) 182 | 183 | src = self.visit(expr.src) 184 | 185 | # If this value can never be larger than a byte, 186 | # then it must be one of the bytes in our swap. 187 | # Add it to a list to check later. 188 | if src is not None and not isinstance(src, (int, int)): 189 | value_range = identify_byte(expr.src, self.function) 190 | if value_range is not None: 191 | self.solver.add( 192 | Or( 193 | src == 0, 194 | And(src <= value_range.end, src >= value_range.step) 195 | ) 196 | ) 197 | 198 | self.byte_vars.add(*expr.src.vars_read) 199 | 200 | if self.byte_values.get( 201 | (value_range.end, value_range.step) 202 | ) is None: 203 | self.byte_values[ 204 | (value_range.end, value_range.step) 205 | ] = simplify(Extract( 206 | int(math.floor(math.log(value_range.end, 2))), 207 | int(math.floor(math.log(value_range.step, 2))), 208 | src 209 | ) 210 | ) 211 | 212 | self.visited.add(expr.dest) 213 | 214 | if expr.instr_index in self.to_visit: 215 | self.to_visit.remove(expr.instr_index) 216 | 217 | if src is not None: 218 | self.solver.add(dest == src) 219 | 220 | def visit_MLIL_VAR_PHI(self, expr): 221 | # MLIL_VAR_PHI doesn't set the size field, so we make do 222 | # with this. 223 | dest = create_BitVec(expr.dest, expr.dest.var.type.width) 224 | 225 | phi_values = [] 226 | 227 | for var in expr.src: 228 | if var not in self.visited: 229 | var_def = self.function.get_ssa_var_definition(var) 230 | self.to_visit.append(var_def) 231 | 232 | src = create_BitVec(var, var.var.type.width) 233 | 234 | # If this value can never be larger than a byte, 235 | # then it must be one of the bytes in our swap. 236 | # Add it to a list to check later. 237 | if src is not None and not isinstance(src, (int, int)): 238 | value_range = identify_byte(var, self.function) 239 | if value_range is not None: 240 | self.solver.add( 241 | Or( 242 | src == 0, 243 | And( 244 | src <= value_range.end, 245 | src >= value_range.step 246 | ) 247 | ) 248 | ) 249 | 250 | self.byte_vars.add(var) 251 | 252 | if self.byte_values.get( 253 | (value_range.end, value_range.step) 254 | ) is None: 255 | self.byte_values[ 256 | (value_range.end, value_range.step) 257 | ] = simplify(Extract( 258 | int( 259 | math.floor( 260 | math.log(value_range.end, 2) 261 | ) 262 | ), 263 | int( 264 | math.floor( 265 | math.log(value_range.step, 2) 266 | ) 267 | ), 268 | src 269 | ) 270 | ) 271 | 272 | phi_values.append(src) 273 | 274 | if phi_values: 275 | phi_expr = reduce( 276 | lambda i, j: Or(i, j), [dest == s for s in phi_values] 277 | ) 278 | 279 | self.solver.add(phi_expr) 280 | 281 | self.visited.add(expr.dest) 282 | if expr.instr_index in self.to_visit: 283 | self.to_visit.remove(expr.instr_index) 284 | 285 | def visit_MLIL_VAR_SSA(self, expr): 286 | if expr.src not in self.visited: 287 | var_def = expr.function.get_ssa_var_definition(expr.src) 288 | if var_def is not None: 289 | self.to_visit.append(var_def) 290 | 291 | src = create_BitVec(expr.src, expr.size) 292 | 293 | value_range = identify_byte(expr, self.function) 294 | if value_range is not None: 295 | self.solver.add( 296 | Or( 297 | src == 0, 298 | And(src <= value_range.end, src >= value_range.step) 299 | ) 300 | ) 301 | 302 | self.byte_vars.add(expr.src) 303 | 304 | return src 305 | 306 | def visit_MLIL_OR(self, expr): 307 | left = self.visit(expr.left) 308 | right = self.visit(expr.right) 309 | 310 | if None not in (left, right): 311 | return left | right 312 | 313 | def visit_MLIL_AND(self, expr): 314 | left = self.visit(expr.left) 315 | right = self.visit(expr.right) 316 | 317 | if None not in (left, right): 318 | return left & right 319 | 320 | def visit_MLIL_LOAD_SSA(self, expr): 321 | src = self.visit(expr.src) 322 | 323 | if src is None: 324 | return 325 | 326 | memory = self._memory 327 | 328 | # we're assuming Little Endian for now 329 | if expr.size == 1: 330 | return memory[src] 331 | elif expr.size == 2: 332 | return Concat(memory[src+1], memory[src]) 333 | elif expr.size == 4: 334 | return Concat( 335 | memory[src+3], 336 | memory[src+2], 337 | memory[src+1], 338 | memory[src] 339 | ) 340 | elif expr.size == 8: 341 | return Concat( 342 | memory[src+7], 343 | memory[src+6], 344 | memory[src+5], 345 | memory[src+4], 346 | memory[src+3], 347 | memory[src+2], 348 | memory[src+1], 349 | memory[src] 350 | ) 351 | 352 | def visit_MLIL_ZX(self, expr): 353 | src = self.visit(expr.src) 354 | 355 | if src is not None: 356 | return ZeroExt( 357 | (expr.size - expr.src.size) * 8, 358 | src 359 | ) 360 | 361 | def visit_MLIL_ADD(self, expr): 362 | left = self.visit(expr.left) 363 | right = self.visit(expr.right) 364 | 365 | if None not in (left, right): 366 | return left + right 367 | 368 | def visit_MLIL_CONST(self, expr): 369 | return expr.constant 370 | 371 | def visit_MLIL_LSL(self, expr): 372 | left = self.visit(expr.left) 373 | right = self.visit(expr.right) 374 | 375 | if None not in (left, right): 376 | return left << right 377 | 378 | def visit_MLIL_LSR(self, expr): 379 | left = self.visit(expr.left) 380 | right = self.visit(expr.right) 381 | 382 | if None not in (left, right): 383 | return LShR(left, right) 384 | 385 | def visit_MLIL_IF(self, expr): 386 | return self.visit(expr.condition) 387 | 388 | def visit_MLIL_CMP_E(self, expr): 389 | left = self.visit(expr.left) 390 | right = self.visit(expr.right) 391 | 392 | if None not in (left, right): 393 | return left == right 394 | 395 | def visit_MLIL_CMP_NE(self, expr): 396 | left = self.visit(expr.left) 397 | right = self.visit(expr.right) 398 | 399 | if None not in (left, right): 400 | return left != right 401 | 402 | def visit_MLIL_CMP_ULT(self, expr): 403 | left = self.visit(expr.left) 404 | right = self.visit(expr.right) 405 | if None not in (left, right): 406 | return ULT(left, right) 407 | 408 | def visit_MLIL_CMP_UGT(self, expr): 409 | left = self.visit(expr.left) 410 | right = self.visit(expr.right) 411 | if None not in (left, right): 412 | return UGT(left, right) 413 | 414 | def visit_MLIL_VAR_SSA_FIELD(self, expr): 415 | if expr.src not in self.visited: 416 | var_def = expr.function.get_ssa_var_definition(expr.src) 417 | if var_def is not None: 418 | self.to_visit.append(var_def) 419 | 420 | var = create_BitVec(expr.src, expr.src.var.type.width) 421 | if expr.offset == 0: 422 | return None 423 | field = Extract( 424 | ((expr.size + expr.offset) * 8) - 1, 425 | expr.offset * 8, 426 | var 427 | ) 428 | 429 | return field 430 | 431 | def visit_MLIL_SET_VAR_SSA_FIELD(self, expr): 432 | # expr.size will be the width of the field, so we need the dest's real 433 | # width 434 | dest = create_BitVec(expr.dest, expr.dest.var.type.width) 435 | prev = create_BitVec(expr.prev, expr.prev.var.type.width) 436 | 437 | mask = (1 << (expr.size * 8) - 1) << (expr.offset * 8) 438 | 439 | mask = ~mask & ((1 << (expr.dest.var.type.width * 8)) - 1) 440 | 441 | src = self.visit(expr.src) 442 | 443 | self.visited.add(expr.dest) 444 | 445 | if expr.instr_index in self.to_visit: 446 | self.to_visit.remove(expr.instr_index) 447 | 448 | if src is not None and expr.offset != 0: 449 | self.solver.add( 450 | dest == ( 451 | (prev & mask) | ZeroExt( 452 | ( 453 | expr.dest.var.type.width - 454 | (expr.size + expr.offset) 455 | ) * 8, 456 | (src << (expr.offset * 8)) 457 | ) 458 | ) 459 | ) 460 | 461 | 462 | 463 | 464 | 465 | 466 | __all__ = ['PluginFindHeartbleed'] 467 | 468 | 469 | class PluginFindHeartbleed(Plugin): 470 | name = "PluginFindHeartbleed" 471 | display_name = "Find Heartbleed" 472 | cmd_name = "heartbleed" 473 | cmd_help = "Search for heartbleed in the openssl version." 474 | 475 | def __init__(self, bv=None): 476 | super(PluginFindHeartbleed, self).__init__(bv) 477 | self.bv = bv 478 | 479 | def set_bv(self, bv): 480 | self.bv = bv 481 | 482 | def run(self, bv=None, deep=None, traces=None): 483 | super(PluginFindHeartbleed, self).__init__(bv) 484 | self._find_heartbleed() 485 | return 486 | 487 | def check_memcpy(self, memcpy_call): 488 | if not hasattr(memcpy_call, "params"): 489 | return False 490 | if len(memcpy_call.params) < 3: 491 | # TODO Binary ninja failed to get the correct parameters 492 | return False 493 | size_param = memcpy_call.params[2] 494 | 495 | if size_param.operation != MediumLevelILOperation.MLIL_VAR_SSA: 496 | return False 497 | 498 | possible_sizes = size_param.possible_values 499 | 500 | # Dataflow won't combine multiple possible values from 501 | # shifted bytes, so any value we care about will be 502 | # undetermined at this point. This might change in the future? 503 | if possible_sizes.type != RegisterValueType.UndeterminedValue: 504 | if hasattr(possible_sizes, "ranges"): 505 | if not possible_sizes.ranges[0].start == 0 and possible_sizes.ranges[0].end == 4294967295: 506 | return False 507 | 508 | model = ByteSwapModeler(size_param, self.bv.address_size) 509 | 510 | return model.is_byte_swap() 511 | 512 | def _find_heartbleed(self): 513 | if not 'memcpy' in self.bv.symbols: 514 | return 515 | if isinstance(self.bv.symbols['memcpy'], list): 516 | _memcpy_Symbol = self.bv.get_code_refs(self.bv.symbols['memcpy'][0].address) 517 | else: 518 | _memcpy_Symbol = self.bv.get_code_refs(self.bv.symbols['memcpy'].address) 519 | memcpy_refs = [ 520 | (ref.function, ref.address) 521 | for ref in _memcpy_Symbol 522 | ] 523 | 524 | print('Checking {} memcpy calls'.format(len(memcpy_refs))) 525 | 526 | dangerous_calls = [] 527 | 528 | for function, addr in tqdm(memcpy_refs, desc=self.name, leave=False): 529 | call_instr = binjaWrapper.get_medium_il_instruction(self.bv, addr) 530 | 531 | #call_instr = function.get_low_level_il_at(addr).medium_level_il 532 | if not call_instr: 533 | continue 534 | if self.check_memcpy(call_instr.ssa_form): 535 | dangerous_calls.append((addr, call_instr.address, call_instr)) 536 | 537 | for call, func, call_instr in dangerous_calls: 538 | text = "{} 0x{:x}\n".format( 539 | self.bv.get_symbol_at(self.bv.get_functions_containing(func)[0].start).name, 540 | call 541 | ) 542 | text += "\t\tthe memcpy function uses a size parameter that potentially comes from an untrusted source" 543 | 544 | vuln = Vulnerability("Untrusted Source in Memcpy!", 545 | text, 546 | call_instr, 547 | "Potential Untrusted Source.", 548 | 70) 549 | self.vulns.append(vuln) -------------------------------------------------------------------------------- /src/avd/reporter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Traxes/zeno/48e72e8884838a171a217336923beec2983853b5/src/avd/reporter/__init__.py -------------------------------------------------------------------------------- /src/avd/reporter/vulnerability.py: -------------------------------------------------------------------------------- 1 | """Module Vulnerability. Safe the Findings in a class 2 | """ 3 | from termcolor import colored 4 | import logging 5 | from ..helper.decorators import accepts 6 | 7 | 8 | class Vulnerability(object): 9 | """Vulnerability Class is used to exchange and describe the findings 10 | 11 | Arguments: 12 | object {[type]} -- [description] 13 | """ 14 | 15 | def __init__(self, bug_class, reason, instr=None, desc=None, prob=0): 16 | """ 17 | Konstruktor. Give it a bug class identifier. Give the Reason a very detailed description and pass it the 18 | vulnurable instruction. 19 | The probability will highlight in the output later 20 | 21 | Example reason 22 | Print something like: 23 | vuln 0x7e7 mov.q [RSI], [RDI] 24 | 25 | Source -> fgets(var_808, 2049); 26 | Potential Overflow! 27 | dst var_808 = 1024 28 | src var_8c9 = 2048 29 | """ 30 | self.bug_class = bug_class 31 | self.instr = instr 32 | self.desc = desc 33 | self.reason = reason 34 | self._probability = prob 35 | 36 | def __hash__(self): 37 | """ 38 | used to fast find duplicates 39 | :return: 40 | """ 41 | return hash(str(self)) 42 | 43 | def __str__(self): 44 | return "{}({})".format(type(self).__name__, 45 | ", ".join(["{}={}".format(k, self.__dict__[k]) for k in sorted(self.__dict__)])) 46 | 47 | def __eq__(self, other): 48 | return isinstance(other, type(self)) and hash(self) == hash(other) 49 | 50 | def __ne__(self, other): 51 | return not self == other 52 | 53 | @property 54 | def probability(self): 55 | """ 56 | Getter for the probability value 57 | :return integer: 58 | """ 59 | return self._probability 60 | 61 | @probability.setter 62 | @accepts(object, int) 63 | def probability(self, number): 64 | """ 65 | Will set the probability of a finding. Higher if more certain 66 | 67 | If the number is out of range. It will automatically use 0 and print a warning 68 | Arguments: 69 | number {[int]} -- [probability from 0-100] 70 | """ 71 | if 0 <= number <= 100: 72 | self._probability = number 73 | else: 74 | logging.warning('Vulnerability probability is out of range (0-100) ! Using 0') 75 | self._probability = 0 76 | return 77 | 78 | @accepts(object, str) 79 | def set_reason(self, reason): 80 | """ 81 | Set the Reason 82 | 83 | Arguments: 84 | reason {[str]} -- [set the reason] 85 | """ 86 | 87 | self.reason = reason 88 | return 89 | 90 | @accepts(object, str) 91 | def append_reason(self, reason): 92 | """ 93 | append text to reason 94 | 95 | Arguments: 96 | reason {[str]} -- [Append to the Reason] 97 | """ 98 | 99 | self.reason = self.reason + "\nNote:\n" + reason 100 | return 101 | 102 | def cmd_print_finding(self, traces=None, bb=None): 103 | """ 104 | Will print the finding in a colored way. But information to be printed is left to the Plugin developer. 105 | It will use according to the probability different color schemes: 106 | 107 | Color - Probability 108 | ----------------------- 109 | red - 80-100 110 | magenta - 60-79 111 | yellow - 40-59 112 | blue - 20-39 113 | white - 0-19 114 | 115 | :return: 116 | """ 117 | x = self.probability 118 | color = ((0 <= x < 20) and "white") or \ 119 | ((20 <= x < 40) and "blue") or \ 120 | ((40 <= x < 60) and "yellow") or \ 121 | ((60 <= x < 80) and "magenta") or \ 122 | ((80 <= x <= 100) and "red") or 'n/a' 123 | if traces: 124 | if bb.start in traces: 125 | print("{}".format(colored("This Vulnerability was hit by your provideded Coverage", "blue"))) 126 | print("Vulnerability:\n{}\nDescription:\n{}\n---".format(colored(self.reason, color), 127 | colored(self.desc, "green"))) 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import binaryninja 4 | import argparse 5 | import os 6 | from avd.loader import PluginLoader 7 | from avd.helper.drcov import DrcovData 8 | import ntpath 9 | import errno 10 | 11 | 12 | def is_valid_file(parser, arg): 13 | """ 14 | Checks if the given argument is a valid file 15 | :param parser: 16 | :param arg: 17 | :return: file path if valid 18 | """ 19 | if not os.path.exists(arg): 20 | parser.error("The file %s does not exist!" % arg) 21 | else: 22 | if not os.path.isfile(arg): 23 | parser.error("The file %s does not exist!" % arg) 24 | else: 25 | return arg # return an open file handle 26 | 27 | 28 | def path_leaf(path): 29 | """ 30 | Gets a Path as argument and returns the filename 31 | :param path: 32 | :return: 33 | """ 34 | head, tail = ntpath.split(path) 35 | return tail or ntpath.basename(head) 36 | 37 | 38 | def plugin_filter(args, plugins): 39 | """ 40 | Filters the available plugins and can order it. 41 | The blacklisting feature will always be the dominant one. 42 | Thus even with Ordering it will filter out the Blacklist. 43 | 44 | Whitelisting will prevent ordering and is mutually exclusive to the blacklisting feature. 45 | If you want to test your ordered Plugin list just use the blacklisting feature to play around. 46 | :param args: 47 | :param plugins: 48 | :return: 49 | """ 50 | returning_plugins = list() 51 | # Blacklist Parsing 52 | if args.blacklist: 53 | for blacklist_module in args.blacklist.replace(" ", "").split(","): 54 | plugins.remove(blacklist_module) 55 | # Whitelist Parsing 56 | elif args.whitelist: 57 | for whitelist_module in args.whitelist.replace(" ", "").split(","): 58 | if whitelist_module in plugins: 59 | returning_plugins.append(whitelist_module) 60 | return returning_plugins 61 | 62 | if args.plugin_order: 63 | # Check if its a list 64 | if "," in args.plugin_order: 65 | for plugin_name in args.plugin_order.replace(" ", "").split(","): 66 | if plugin_name in plugins: 67 | returning_plugins.append(plugin_name) 68 | else: 69 | # assume the given argument is a path to a file 70 | if not os.path.exists(args.plugin_order): 71 | OSError.NotADirectoryError(errno.ENOENT, os.strerror(errno.ENOENT), args.plugin_order) 72 | else: 73 | if not os.path.isfile(args.plugin_order): 74 | raise OSError.FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), args.plugin_order) 75 | else: 76 | # Parse the given file (Plugins splitted by newlines) 77 | with open(args.plugin_order) as fin: 78 | for plugin_name in fin: 79 | if plugin_name in plugins: 80 | returning_plugins.append(plugin_name) 81 | 82 | return returning_plugins if len(returning_plugins) > 0 else plugins 83 | 84 | 85 | def main(): 86 | """ 87 | Main function. Also used for Commandline Parsing 88 | :return: 89 | """ 90 | 91 | parser = argparse.ArgumentParser(description='Zeno Commandline tool: Searches Automagically for Bugs') 92 | group = parser.add_mutually_exclusive_group() 93 | group.add_argument('-b', '--blacklist', type=str, 94 | help="Provide a blacklist seperated by commas. This will filter out not needed plugins") 95 | group.add_argument('-wl', '--whitelist', help='Whitelist modules', type=str) 96 | 97 | parser.add_argument("--plugin_order", default=None, type=str, 98 | dest="plugin_order", help="Provide a file with the plugins in the correct order to be loaded") 99 | 100 | parser.add_argument('--deep', 101 | dest='deep', action='store_true', 102 | help='Uses Deep Search mode. ' 103 | 'This might take longer but it will also get a grasp of compiler optimizations') 104 | parser.add_argument('--fast', 105 | dest='fast', action='store_true', 106 | help='Uses Fast Search mode. ' 107 | 'It will skip a throughout search while slicing and just uses the first it finds') 108 | parser.add_argument('--search-path', 109 | dest='search_path', default="/lib:/usr/lib", 110 | help='":" separated list of paths to search libraries in') 111 | parser.add_argument('target', metavar='target-path', nargs='+', 112 | help='Binary to be analysed', 113 | type=lambda x: is_valid_file(parser, x)) 114 | parser.add_argument("--system-root", default="/", 115 | dest="root", help="Use paths relative to this root for library searching") 116 | 117 | parser.add_argument("--dot", default=None, 118 | dest="outputfile", help="Write graph to a dotfile") 119 | 120 | parser.add_argument("--cov", default=None, 121 | dest="coverage", help="Provide a coverage file for better filtering") 122 | 123 | parser.add_argument("--cov_folder", default=None, 124 | dest="cov_folder", help="Provide a folder with coverage files for better filtering") 125 | 126 | plugins = PluginLoader(argparser=parser) 127 | args = parser.parse_args() 128 | 129 | filtered_plugins = plugin_filter(args, [name for name, _ in plugins.available_plugins]) 130 | 131 | # Start Working with the Binaries here 132 | input_file = args.target 133 | for filename in input_file: 134 | print("Analyzing {0}".format(filename)) 135 | bv = binaryninja.BinaryViewType.get_view_of_file(filename) 136 | if args.coverage: 137 | print("Single Coverage given") 138 | cov = DrcovData(args.coverage) 139 | cov_bb = cov.get_blocks_by_module(path_leaf(filename)) 140 | # TODO Insert to Plugin Analyser 141 | if args.cov_folder: 142 | # TODO Make multi Coverage possible 143 | pass 144 | 145 | print("arch: {0} | platform: {1}".format(bv.arch, bv.platform)) 146 | bv.update_analysis_and_wait() 147 | 148 | print(filtered_plugins) 149 | for name in filtered_plugins: 150 | plugin = plugins.get_plugin_instance(name) 151 | plugin.vulns = [] 152 | #try: 153 | plugin.run(bv, args) 154 | #except: 155 | # Catch All to Continue with the next Plugin 156 | # print("Plugin {} did fail for some Reason. Continuing with the next one".format(name)) 157 | # pass 158 | if args.coverage: 159 | plugin.set_traces(cov_bb) 160 | del plugin # This will print the vulns. 161 | 162 | return 163 | 164 | 165 | if __name__ == "__main__": 166 | main() 167 | -------------------------------------------------------------------------------- /src/tests/test_plugins.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import binaryninja 3 | 4 | from src.avd.loader import PluginLoader 5 | 6 | 7 | class ArgParseMock(object): 8 | """ 9 | Mocking argparse 10 | """ 11 | def __init__(self, deep, fast): 12 | self.deep = deep 13 | self.fast = fast 14 | 15 | 16 | class TestBufferOverflows(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self._plugins = PluginLoader() 20 | 21 | def test_buffer_overflow_1(self): 22 | """ 23 | Testcase to find vuln if dest and source size are known but n is 24 | :return: 25 | """ 26 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test1/bo") 27 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 28 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 29 | plugin.vulns = [] 30 | args = ArgParseMock(False, False) 31 | plugin.run(bv, args) 32 | self.assertIsNone(plugin.error), 'An error occurred' 33 | addresses = [] 34 | highprob = 0 35 | for vuln in plugin.vulns: 36 | addresses.append(vuln.instr.address) 37 | highprob = vuln.probability if vuln.probability > highprob else highprob 38 | 39 | self.assertIn(0x829, addresses), 'Could not find the Bug' 40 | self.assertGreater(highprob, 89), 'Found the initial one but could not follow to get the source' 41 | 42 | def test_buffer_overflow_2(self): 43 | """ 44 | Testcase to find vuln for N is bigger than destination size 45 | :return: 46 | """ 47 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test2/bo") 48 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 49 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 50 | plugin.vulns = [] 51 | plugin.run(bv, False) 52 | self.assertIsNone(plugin.error), 'An error occurred' 53 | addresses = [] 54 | highprob = 0 55 | for vuln in plugin.vulns: 56 | addresses.append(vuln.instr.address) 57 | highprob = vuln.probability if vuln.probability > highprob else highprob 58 | 59 | self.assertIn(0x825, addresses), 'Could not find the Bug' 60 | self.assertGreater(highprob, 90), 'Could not find the bug' 61 | 62 | def test_buffer_overflow_3(self): 63 | """ 64 | Testcase to find 2 Vulnerabilities same as in Testcase "test_buffer_overflow_2" where N is bigger than 65 | Destination Size. 66 | :return: 67 | """ 68 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test3/bo") 69 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 70 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 71 | plugin.vulns = [] 72 | plugin.run(bv, False) 73 | self.assertIsNone(plugin.error), 'An error occurred' 74 | addresses = [] 75 | highprob = 0 76 | for vuln in plugin.vulns: 77 | addresses.append(vuln.instr.address) 78 | highprob = vuln.probability if vuln.probability > highprob else highprob 79 | self.assertGreater(highprob, 90), 'All Vulnerabilities here should be higher than 90' 80 | 81 | self.assertIn(0x825, addresses), 'Could not find the Bug' 82 | self.assertIn(0x807, addresses), 'Could not find the Bug' 83 | 84 | 85 | def test_buffer_overflow_4(self): 86 | """ 87 | Testcase to find occurances of gets and fgets with only a single Variable as destination Buffer 88 | :return: 89 | """ 90 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test4/bo") 91 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 92 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 93 | plugin.vulns = [] 94 | plugin.run(bv, False) 95 | self.assertIsNone(plugin.error), 'An error occurred' 96 | addresses = [] 97 | highprob = 0 98 | for vuln in plugin.vulns: 99 | addresses.append(vuln.instr.address) 100 | highprob = vuln.probability if vuln.probability > highprob else highprob 101 | 102 | self.assertIn(0x715, addresses), 'Could not find the Bug' 103 | self.assertGreater(highprob, 90), 'All Vulnerabilities here should be higher than 90' 104 | 105 | def test_buffer_overflow_5(self): 106 | """ 107 | Testcase to find Compiler optimized Memory Copy functions It will also test if the destination is smaller 108 | than the source 109 | :return: 110 | """ 111 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test5/bo") 112 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 113 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 114 | plugin.vulns = [] 115 | args = ArgParseMock(True, False) 116 | plugin.run(bv, args) 117 | self.assertIsNone(plugin.error), 'An error occurred' 118 | addresses = [] 119 | highprob = 0 120 | for vuln in plugin.vulns: 121 | addresses.append(vuln.instr.address) 122 | highprob = vuln.probability if vuln.probability > highprob else highprob 123 | 124 | self.assertIn(0x76f, addresses), 'Could not find the Bug' 125 | self.assertGreater(highprob, 79), 'Could not follow to find the source' 126 | 127 | def test_buffer_overflow_6(self): 128 | """ 129 | Testcase to find two Vulnerabilities. One in fgets and one in strcpy 130 | :return: 131 | """ 132 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test6/bo") 133 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 134 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 135 | plugin.vulns = [] 136 | args = ArgParseMock(True, False) 137 | plugin.run(bv, args) 138 | self.assertIsNone(plugin.error), 'An error occurred' 139 | addresses = [] 140 | highprob = 0 141 | for vuln in plugin.vulns: 142 | addresses.append(vuln.instr.address) 143 | highprob = vuln.probability if vuln.probability > highprob else highprob 144 | 145 | self.assertIn(0x7e7, addresses), 'Could not find the fgets Bug' 146 | self.assertIn(0x800, addresses), 'Could not find the strcpy Bug' 147 | self.assertGreater(highprob, 79), 'Could not follow to find the source' 148 | 149 | def test_buffer_overflow_7(self): 150 | """ 151 | Testcase to find two Vulnerabilities. One in fgets and one in strcpy 152 | :return: 153 | """ 154 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test7/bo") 155 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 156 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 157 | plugin.vulns = [] 158 | args = ArgParseMock(True, False) 159 | plugin.run(bv, args) 160 | self.assertIsNone(plugin.error), 'An error occurred' 161 | addresses = [] 162 | highprob = 0 163 | for vuln in plugin.vulns: 164 | addresses.append(vuln.instr.address) 165 | highprob = vuln.probability if vuln.probability > highprob else highprob 166 | 167 | self.assertIn(0x7f7, addresses), 'Could not find the fgets Bug' 168 | self.assertIn(0x815, addresses), 'Could not find the strncpy Bug' 169 | self.assertGreater(highprob, 79), 'Could not follow to find the source' 170 | 171 | def test_buffer_overflow_8(self): 172 | """ 173 | Testcase to find two Vulnerabilities. One in fgets and one in strcpy 174 | :return: 175 | """ 176 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test8/bo") 177 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 178 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 179 | plugin.vulns = [] 180 | args = ArgParseMock(True, False) 181 | plugin.run(bv, args) 182 | self.assertIsNone(plugin.error), 'An error occurred' 183 | addresses = [] 184 | highprob = 0 185 | for vuln in plugin.vulns: 186 | addresses.append(vuln.instr.address) 187 | highprob = vuln.probability if vuln.probability > highprob else highprob 188 | 189 | self.assertIn(0x800, addresses), 'Could not find the strncpy Bug' 190 | self.assertGreater(highprob, 79), 'Could not follow to find the source' 191 | 192 | def test_buffer_overflow_9(self): 193 | """ 194 | Testcase to find two Vulnerabilities. One in fgets and one in strcpy 195 | :return: 196 | """ 197 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test9/bo") 198 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 199 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 200 | plugin.vulns = [] 201 | args = ArgParseMock(True, False) 202 | plugin.run(bv, args) 203 | self.assertIsNone(plugin.error), 'An error occurred' 204 | addresses = [] 205 | highprob = 0 206 | for vuln in plugin.vulns: 207 | addresses.append(vuln.instr.address) 208 | highprob = vuln.probability if vuln.probability > highprob else highprob 209 | 210 | self.assertIn(0x809, addresses), 'Could not find the sprintf Bug' 211 | self.assertIn(0x7e7, addresses), 'Could not find the sprintf Bug' 212 | self.assertGreater(highprob, 60), 'Could not follow to find the source' 213 | 214 | def test_buffer_overflow_10(self): 215 | """ 216 | Testcase to find no Vulnerabilities. 217 | :return: 218 | """ 219 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test10/bo") 220 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 221 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 222 | args = ArgParseMock(True, False) 223 | plugin.run(bv, args) 224 | self.assertIsNone(plugin.error), 'An error occurred' 225 | addresses = [] 226 | highprob = 0 227 | for vuln in plugin.vulns: 228 | addresses.append(vuln.instr.address) 229 | highprob = vuln.probability if vuln.probability > highprob else highprob 230 | self.assertFalse(bool(addresses)), 'Found bugs where no bugs are' 231 | 232 | def test_buffer_overflow_11(self): 233 | """ 234 | Testcase to find one Vulnerability in scanf. 235 | :return: 236 | """ 237 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test11/bo") 238 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 239 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 240 | args = ArgParseMock(True, False) 241 | plugin.run(bv, args) 242 | self.assertIsNone(plugin.error), 'An error occurred' 243 | addresses = [] 244 | highprob = 0 245 | for vuln in plugin.vulns: 246 | addresses.append(vuln.instr.address) 247 | highprob = vuln.probability if vuln.probability > highprob else highprob 248 | 249 | self.assertIn(0x754, addresses), 'Could not find the scanf Bug' 250 | self.assertGreater(highprob, 60), 'Could not follow to find the source' 251 | 252 | def test_buffer_overflow_12(self): 253 | """ 254 | Testcase to find a scanf Vulnerability. 255 | :return: 256 | """ 257 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test12/bo") 258 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 259 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 260 | args = ArgParseMock(True, False) 261 | plugin.run(bv, args) 262 | self.assertIsNone(plugin.error), 'An error occurred' 263 | addresses = [] 264 | highprob = 0 265 | for vuln in plugin.vulns: 266 | addresses.append(vuln.instr.address) 267 | highprob = vuln.probability if vuln.probability > highprob else highprob 268 | 269 | self.assertIn(0x754, addresses), 'Could not find the scanf Bug' 270 | self.assertGreater(highprob, 60), 'Could not follow to find the source' 271 | 272 | def test_buffer_overflow_13(self): 273 | """ 274 | Testcase to find two scanf Vulnerabilities. 275 | :return: 276 | """ 277 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/Test13/bo") 278 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 279 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 280 | args = ArgParseMock(True, False) 281 | plugin.run(bv, args) 282 | self.assertIsNone(plugin.error), 'An error occurred' 283 | addresses = [] 284 | highprob = 0 285 | for vuln in plugin.vulns: 286 | addresses.append(vuln.instr.address) 287 | highprob = vuln.probability if vuln.probability > highprob else highprob 288 | 289 | self.assertIn(0x754, addresses), 'Could not find the first scanf Bug' 290 | self.assertIn(0x76c, addresses), 'Could not find the second scanf Bug' 291 | self.assertGreater(highprob, 60), 'Could not follow to find the source' 292 | 293 | def test_buffer_overflow_memcpy_1(self): 294 | """ 295 | Testcase to find an often occurring memcpy pattern. 296 | :return: 297 | """ 298 | bv = binaryninja.BinaryViewType.get_view_of_file("./bin/TestMcpy/bo") 299 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 300 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 301 | args = ArgParseMock(True, False) 302 | plugin.run(bv, args) 303 | self.assertIsNone(plugin.error), 'An error occurred' 304 | addresses = [] 305 | highprob = 0 306 | for vuln in plugin.vulns: 307 | addresses.append(vuln.instr.address) 308 | highprob = vuln.probability if vuln.probability > highprob else highprob 309 | 310 | self.assertIn(0x7cc, addresses), 'Could not find the first scanf Bug' 311 | self.assertGreater(highprob, 79), 'Could not follow to find the source' 312 | 313 | def CWE121_Stack_Based_Buffer_Overflow__char_type_overrun_memcpy_01(self): 314 | """ 315 | @description 316 | CWE: 121 Stack Based Buffer Overflow 317 | Sinks: type_overrun_memcpy 318 | GoodSink: Perform the memcpy() and prevent overwriting part of the structure 319 | BadSink : Overwrite part of the structure by incorrectly using the sizeof(struct) in memcpy() 320 | Flow Variant: 01 Baseline 321 | Testcase to find an often occurring memcpy pattern. 322 | :return: 323 | """ 324 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/CWE121_Stack_Based_Buffer_Overflow/s01/" 325 | "CWE121_Stack_Based_Buffer_Overflow__char_type_overrun_" 326 | "memcpy_01.out") 327 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 328 | self.assertIsNotNone(plugin), 'Could not load Plugin Buffer Overflow' 329 | plugin.run(bv, deep=True) 330 | self.assertIsNone(plugin.error), 'An error occurred' 331 | addresses = [] 332 | highprob = 0 333 | for vuln in plugin.vulns: 334 | addresses.append(vuln.instr.address) 335 | highprob = vuln.probability if vuln.probability > highprob else highprob 336 | 337 | self.assertIn(0xd22, addresses), 'Could not find the memcpy Bug' 338 | self.assertGreater(highprob, 79), 'Could not follow to find the source' 339 | 340 | def test_uninitialized_variable_01(self): 341 | """ 342 | Testcase to find an uninitialized variable. 343 | :return: 344 | """ 345 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 346 | "CWE457_Use_of_Uninitialized_Variable/s01/" 347 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_01.out") 348 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 349 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 350 | args = ArgParseMock(True, False) 351 | plugin.run(bv, args) 352 | self.assertIsNone(plugin.error), 'An error occurred' 353 | addresses = [] 354 | highprob = 0 355 | for vuln in plugin.vulns: 356 | addresses.append(vuln.instr.address) 357 | highprob = vuln.probability if vuln.probability > highprob else highprob 358 | 359 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 360 | 361 | def test_uninitialized_variable_02(self): 362 | """ 363 | Testcase to find an uninitialized variable. 364 | :return: 365 | """ 366 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 367 | "CWE457_Use_of_Uninitialized_Variable/s01/" 368 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_02.out") 369 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 370 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 371 | args = ArgParseMock(True, False) 372 | plugin.run(bv, args) 373 | self.assertIsNone(plugin.error), 'An error occurred' 374 | addresses = [] 375 | highprob = 0 376 | for vuln in plugin.vulns: 377 | addresses.append(vuln.instr.address) 378 | highprob = vuln.probability if vuln.probability > highprob else highprob 379 | 380 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 381 | 382 | def test_uninitialized_variable_03(self): 383 | """ 384 | Testcase to find an uninitialized variable. 385 | :return: 386 | """ 387 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 388 | "CWE457_Use_of_Uninitialized_Variable/s01/" 389 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_03.out") 390 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 391 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 392 | args = ArgParseMock(True, False) 393 | plugin.run(bv, args) 394 | self.assertIsNone(plugin.error), 'An error occurred' 395 | addresses = [] 396 | highprob = 0 397 | for vuln in plugin.vulns: 398 | addresses.append(vuln.instr.address) 399 | highprob = vuln.probability if vuln.probability > highprob else highprob 400 | 401 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 402 | 403 | def test_uninitialized_variable_04(self): 404 | """ 405 | Testcase to find an uninitialized variable. 406 | :return: 407 | """ 408 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 409 | "CWE457_Use_of_Uninitialized_Variable/s01/" 410 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_04.out") 411 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 412 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 413 | args = ArgParseMock(True, False) 414 | plugin.run(bv, args) 415 | self.assertIsNone(plugin.error), 'An error occurred' 416 | addresses = [] 417 | highprob = 0 418 | for vuln in plugin.vulns: 419 | addresses.append(vuln.instr.address) 420 | highprob = vuln.probability if vuln.probability > highprob else highprob 421 | 422 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 423 | 424 | def test_uninitialized_variable_05(self): 425 | """ 426 | Testcase to find an uninitialized variable. 427 | :return: 428 | """ 429 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 430 | "CWE457_Use_of_Uninitialized_Variable/s01/" 431 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_05.out") 432 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 433 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 434 | args = ArgParseMock(True, False) 435 | plugin.run(bv, args) 436 | self.assertIsNone(plugin.error), 'An error occurred' 437 | addresses = [] 438 | highprob = 0 439 | for vuln in plugin.vulns: 440 | addresses.append(vuln.instr.address) 441 | highprob = vuln.probability if vuln.probability > highprob else highprob 442 | 443 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 444 | 445 | def test_uninitialized_variable_06(self): 446 | """ 447 | Testcase to find an uninitialized variable. 448 | :return: 449 | """ 450 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 451 | "CWE457_Use_of_Uninitialized_Variable/s01/" 452 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_06.out") 453 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 454 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 455 | args = ArgParseMock(True, False) 456 | plugin.run(bv, args) 457 | self.assertIsNone(plugin.error), 'An error occurred' 458 | addresses = [] 459 | highprob = 0 460 | for vuln in plugin.vulns: 461 | addresses.append(vuln.instr.address) 462 | highprob = vuln.probability if vuln.probability > highprob else highprob 463 | 464 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 465 | 466 | def test_uninitialized_variable_07(self): 467 | """ 468 | Testcase to find an uninitialized variable. 469 | :return: 470 | """ 471 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 472 | "CWE457_Use_of_Uninitialized_Variable/s01/" 473 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_07.out") 474 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 475 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 476 | args = ArgParseMock(True, False) 477 | plugin.run(bv, args) 478 | self.assertIsNone(plugin.error), 'An error occurred' 479 | addresses = [] 480 | highprob = 0 481 | for vuln in plugin.vulns: 482 | addresses.append(vuln.instr.address) 483 | highprob = vuln.probability if vuln.probability > highprob else highprob 484 | 485 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 486 | 487 | def test_uninitialized_variable_08(self): 488 | """ 489 | Testcase to find an uninitialized variable. 490 | :return: 491 | """ 492 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 493 | "CWE457_Use_of_Uninitialized_Variable/s01/" 494 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_08.out") 495 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 496 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 497 | args = ArgParseMock(True, False) 498 | plugin.run(bv, args) 499 | self.assertIsNone(plugin.error), 'An error occurred' 500 | addresses = [] 501 | highprob = 0 502 | for vuln in plugin.vulns: 503 | addresses.append(vuln.instr.address) 504 | highprob = vuln.probability if vuln.probability > highprob else highprob 505 | 506 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 507 | 508 | def test_uninitialized_variable_09(self): 509 | """ 510 | Testcase to find an uninitialized variable. 511 | :return: 512 | """ 513 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 514 | "CWE457_Use_of_Uninitialized_Variable/s01/" 515 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_09.out") 516 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 517 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 518 | args = ArgParseMock(True, False) 519 | plugin.run(bv, args) 520 | self.assertIsNone(plugin.error), 'An error occurred' 521 | addresses = [] 522 | highprob = 0 523 | for vuln in plugin.vulns: 524 | addresses.append(vuln.instr.address) 525 | highprob = vuln.probability if vuln.probability > highprob else highprob 526 | 527 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 528 | 529 | def test_uninitialized_variable_10(self): 530 | """ 531 | Testcase to find an uninitialized variable. 532 | :return: 533 | """ 534 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 535 | "CWE457_Use_of_Uninitialized_Variable/s01/" 536 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_10.out") 537 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 538 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 539 | args = ArgParseMock(True, False) 540 | plugin.run(bv, args) 541 | self.assertIsNone(plugin.error), 'An error occurred' 542 | addresses = [] 543 | highprob = 0 544 | for vuln in plugin.vulns: 545 | addresses.append(vuln.instr.address) 546 | highprob = vuln.probability if vuln.probability > highprob else highprob 547 | 548 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 549 | 550 | def test_uninitialized_variable_11(self): 551 | """ 552 | Testcase to find an uninitialized variable. 553 | :return: 554 | """ 555 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 556 | "CWE457_Use_of_Uninitialized_Variable/s01/" 557 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_11.out") 558 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 559 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 560 | args = ArgParseMock(True, False) 561 | plugin.run(bv, args) 562 | self.assertIsNone(plugin.error), 'An error occurred' 563 | addresses = [] 564 | highprob = 0 565 | for vuln in plugin.vulns: 566 | addresses.append(vuln.instr.address) 567 | highprob = vuln.probability if vuln.probability > highprob else highprob 568 | 569 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 570 | 571 | def test_uninitialized_variable_12(self): 572 | """ 573 | Testcase to find an uninitialized variable. 574 | :return: 575 | """ 576 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 577 | "CWE457_Use_of_Uninitialized_Variable/s01/" 578 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_12.out") 579 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 580 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 581 | args = ArgParseMock(True, False) 582 | plugin.run(bv, args) 583 | self.assertIsNone(plugin.error), 'An error occurred' 584 | addresses = [] 585 | highprob = 0 586 | for vuln in plugin.vulns: 587 | addresses.append(vuln.instr.address) 588 | highprob = vuln.probability if vuln.probability > highprob else highprob 589 | 590 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 591 | 592 | def test_uninitialized_variable_13(self): 593 | """ 594 | Testcase to find an uninitialized variable. 595 | :return: 596 | """ 597 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 598 | "CWE457_Use_of_Uninitialized_Variable/s01/" 599 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_13.out") 600 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 601 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 602 | args = ArgParseMock(True, False) 603 | plugin.run(bv, args) 604 | self.assertIsNone(plugin.error), 'An error occurred' 605 | addresses = [] 606 | highprob = 0 607 | for vuln in plugin.vulns: 608 | addresses.append(vuln.instr.address) 609 | highprob = vuln.probability if vuln.probability > highprob else highprob 610 | 611 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 612 | 613 | def test_uninitialized_variable_15(self): 614 | """ 615 | Testcase to find an uninitialized variable. 616 | :return: 617 | """ 618 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 619 | "CWE457_Use_of_Uninitialized_Variable/s01/" 620 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_15.out") 621 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 622 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 623 | args = ArgParseMock(True, False) 624 | plugin.run(bv, args) 625 | self.assertIsNone(plugin.error), 'An error occurred' 626 | addresses = [] 627 | highprob = 0 628 | for vuln in plugin.vulns: 629 | addresses.append(vuln.instr.address) 630 | highprob = vuln.probability if vuln.probability > highprob else highprob 631 | 632 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 633 | 634 | def test_uninitialized_variable_17(self): 635 | """ 636 | Testcase to find an uninitialized variable. 637 | :return: 638 | """ 639 | bv = binaryninja.BinaryViewType.get_view_of_file("./juliet/" 640 | "CWE457_Use_of_Uninitialized_Variable/s01/" 641 | "CWE457_Use_of_Uninitialized_Variable__char_pointer_17.out") 642 | plugin = self._plugins.get_plugin_instance('PluginUninitializedVariable') 643 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginUninitializedVariable' 644 | args = ArgParseMock(True, False) 645 | plugin.run(bv, args) 646 | self.assertIsNone(plugin.error), 'An error occurred' 647 | addresses = [] 648 | highprob = 0 649 | for vuln in plugin.vulns: 650 | addresses.append(vuln.instr.address) 651 | highprob = vuln.probability if vuln.probability > highprob else highprob 652 | 653 | self.assertIn(0xc98, addresses), 'Could not find the Uninitialized Variable' 654 | 655 | def test_signed_problem_malloc(self): 656 | """ 657 | Testcase to find an uninitialized variable. 658 | :return: 659 | """ 660 | bv = binaryninja.BinaryViewType.get_view_of_file( 661 | "juliet/" 662 | "CWE195_Signed_to_Unsigned_Conversion_error/s01/" 663 | "CWE195_Signed_to_Unsigned_Conversion_Error__connect_socket_malloc_01.out" 664 | ) 665 | plugin = self._plugins.get_plugin_instance('PluginSignedAnalysis') 666 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginSignedAnalysis' 667 | args = ArgParseMock(True, False) 668 | plugin.run(bv, args) 669 | self.assertIsNone(plugin.error), 'An error occurred' 670 | addresses = [] 671 | highprob = 0 672 | for vuln in plugin.vulns: 673 | addresses.append(vuln.instr.address) 674 | highprob = vuln.probability if vuln.probability > highprob else highprob 675 | 676 | self.assertIn(0x100f, addresses), 'Could not find the malloc Problem' 677 | 678 | def test_heartbleed(self): 679 | """ 680 | Testcase to find Heartbleed in a Vulnerable OpenSSL version. 681 | :return: 682 | """ 683 | bv = binaryninja.BinaryViewType.get_view_of_file( 684 | "bin/Heartbleed/openssl.bndb" 685 | ) 686 | plugin = self._plugins.get_plugin_instance('PluginFindHeartbleed') 687 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginFindHeartbleed' 688 | args = ArgParseMock(True, False) 689 | plugin.run(bv, args) 690 | self.assertIsNone(plugin.error), 'An error occurred' 691 | addresses = [] 692 | highprob = 69 693 | for vuln in plugin.vulns: 694 | addresses.append(vuln.instr.address) 695 | highprob = vuln.probability if vuln.probability > highprob else highprob 696 | 697 | self.assertIn(0x80af204, addresses), 'Could not find the Hearbleed Problem' 698 | 699 | def test_trustlet(self): 700 | """ 701 | Testcase to find bugs in ARM Trustlets from the Samsung S6. 702 | :return: 703 | """ 704 | bv = binaryninja.BinaryViewType.get_view_of_file( 705 | "bin/Trustlets/fffffffff0000000000000000000001b.tlbin.bndb" 706 | ) 707 | plugin = self._plugins.get_plugin_instance('PluginBufferOverflow') 708 | self.assertIsNotNone(plugin), 'Could not load Plugin PluginBufferOverflow' 709 | args = ArgParseMock(False, False) 710 | plugin.run(bv, args) 711 | self.assertIsNone(plugin.error), 'An error occurred' 712 | addresses = [] 713 | highprob = 69 714 | for vuln in plugin.vulns: 715 | addresses.append(vuln.instr.address) 716 | highprob = vuln.probability if vuln.probability > highprob else highprob 717 | 718 | self.assertIn(0x15be, addresses), 'Could not find the Memcpy Overflow in function 0x15a8' 719 | self.assertIn(0x2272, addresses), 'Could not find the Memcpy Overflow in function 0x1dee' 720 | 721 | if __name__ == '__main__': 722 | unittest.main() -------------------------------------------------------------------------------- /src/tests/test_slice.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import binaryninja 3 | 4 | from src.avd.loader import PluginLoader 5 | from src.avd.core.sliceEngine.slice import SliceEngine 6 | from src.avd.helper import binjaWrapper 7 | 8 | 9 | class ArgParseMock(object): 10 | """ 11 | Mocking argparse 12 | """ 13 | def __init__(self, deep, fast): 14 | self.deep = deep 15 | self.fast = fast 16 | 17 | 18 | class TestSliceEngine(unittest.TestCase): 19 | 20 | def setUp(self): 21 | self._plugins = PluginLoader() 22 | 23 | def test_slice_engine_1(self): 24 | """ 25 | Testcase to test the slice engine to work properly 26 | :return: 27 | """ 28 | bv = binaryninja.BinaryViewType.get_view_of_file("./SliceEngine/Test1/slice.bndb") 29 | symbol = bv.get_symbol_by_raw_name("memcpy") 30 | args = ArgParseMock(False, False) 31 | if symbol is not None: 32 | for ref in bv.get_code_refs(symbol.address): 33 | instr = binjaWrapper.get_medium_il_instruction(bv, ref.address) 34 | dest_var = binjaWrapper.get_ssa_var_from_mlil_instruction(instr, 0) 35 | slice_class = SliceEngine(args=args, bv=bv) 36 | visited_instructions = slice_class.do_backward_slice_with_variable( 37 | instr, 38 | binjaWrapper.get_mlil_function(bv, ref.address).ssa_form, 39 | dest_var, 40 | list() 41 | ) 42 | print(visited_instructions) 43 | 44 | 45 | if __name__ == '__main__': 46 | unittest.main() 47 | --------------------------------------------------------------------------------