├── .gitignore ├── requirements.txt ├── demo.gif ├── components ├── __init__.py ├── SettingsModule.py ├── utils.py ├── ProgramState.py ├── views │ ├── CFGView.py │ ├── LeftSideBar.py │ ├── LinearView.py │ └── HexView.py ├── MainMenuContext.py └── BinaryNinjaContext.py ├── Logos ├── small.logo ├── medium.logo └── large.logo ├── settings.json ├── LICENSE ├── nehebn2.py └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyfiglet 2 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElykDeer/nehebn2/HEAD/demo.gif -------------------------------------------------------------------------------- /components/__init__.py: -------------------------------------------------------------------------------- 1 | from components.utils import * 2 | from components.SettingsModule import * 3 | from components.MainMenuContext import * 4 | from components.BinaryNinjaContext import * 5 | from components.ProgramState import * 6 | -------------------------------------------------------------------------------- /Logos/small.logo: -------------------------------------------------------------------------------- 1 | @@@@@@@* .@@@@@@@@@@@@@@@@@. *@@@@@@@ 2 | @@@@@* .@@@@@@###########@@@@@@. *@@@@@ 3 | @@@@*.@@@@###################@@@@. *@@@ 4 | @@* .@@@#######################@@@@ *@@ 5 | @*.@@@###########################@@@ *@ 6 | .@@@#############################@@@ 7 | .@@@################################@@ 8 | @@@@##%@@@@@@@@@@@@@@@@@@@@@@@@@@##@@@. 9 | @@@@##%@@@@@@@@@@@@@@@@@@@@@@@@@@##&@@@ 10 | @@@@##%@@&/ * \@@@##&@@@ 11 | *@@@###@@ @@@@@ @@##@@@* 12 | @@@%##@@@\ /@@@@@#&@@@\ /@@@###@@* 13 | . @@@#####@@@@@@#######@@@@@@%####@@* . 14 | @. *@@%##########################@@* .@ 15 | @@. *@@@#######################@@@* .@@ 16 | @@@\ *@@@%#################%@@@* .@@@ 17 | @@@@@. *#@@@@@%########%@@@@@#* .@@@@@ 18 | @@@@@@@. *#@@@@@@@@@@@@@@@@#* .@@@@@@@ -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version" : "1", 3 | "Key_Shutdown" : "KEY_F(1)", 4 | "functionListScreenWidth" : 40, 5 | "xrefsScreenHeight" : 15, 6 | "BinaryNinjaContextDualFocus" : false, 7 | "BinaryNinjaContextSwitchView" : " ", 8 | "BinaryNinjaContextSwitchFocus" : "\t", 9 | "linearDisassemblyScrollDown" : "KEY_DOWN", 10 | "linearDisassemblyScrollUp" : "KEY_UP", 11 | "functionListScrollDown" : "KEY_DOWN", 12 | "functionListScrollUp" : "KEY_UP", 13 | "hexViewLineDown" : "KEY_DOWN", 14 | "hexViewLineUp" : "KEY_UP", 15 | "linearDisassemblyPageDown" : "KEY_NPAGE", 16 | "linearDisassemblyPageUp" : "KEY_PPAGE", 17 | "functionListPageDown" : "KEY_NPAGE", 18 | "functionListPageUp" : "KEY_PPAGE", 19 | "hexViewPageDown" : "KEY_NPAGE", 20 | "hexViewPageUp" : "KEY_PPAGE", 21 | "hexViewRight" : "KEY_RIGHT", 22 | "hexViewLeft" : "KEY_LEFT", 23 | "hexLineLength" : 24, 24 | "functionListSelect" : "\n", 25 | "popupWindowGoto" : "g", 26 | "confirm" : "\n" 27 | } -------------------------------------------------------------------------------- /components/SettingsModule.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class SettingsModule(): 5 | """ A class to serve as a buffer for user settings """ 6 | def __init__(self): 7 | self.filename = "settings.json" 8 | self.dirty = False # 'Changed value' flag 9 | self.load() 10 | 11 | def __getitem__(self, key): 12 | """ Fetch a setting """ 13 | return self.settingsObject[key] 14 | 15 | def __setitem__(self, key, value): 16 | """ Add new options or update old ones """ 17 | self.dirty = True 18 | self.settingsObject[key] = value 19 | 20 | def __del__(self): 21 | """ Upon destruction of this object, save its data """ 22 | if self.dirty: 23 | self.save() 24 | 25 | def load(self): 26 | """ Load settings from a file """ 27 | with open(self.filename) as settingsFile: 28 | self.settingsObject = json.load(settingsFile) 29 | 30 | def save(self): 31 | """ Save the settings object to a file """ 32 | with open(self.filename, 'w') as settingsFile: 33 | json.dump(self.settingsObject, settingsFile) 34 | -------------------------------------------------------------------------------- /components/utils.py: -------------------------------------------------------------------------------- 1 | # Give it a string and it will seperate at newlines 2 | def drawMultiLineText(y, x, string, screen): 3 | for yLine,line in zip(range(y, y+len(string.split('\n'))), string.split('\n')): 4 | screen.addstr(yLine, x, line) 5 | 6 | 7 | # Each element of the list goes on a new line 8 | def drawMultiLineList(y, x, stringList, screen): 9 | for yLine,line in zip(range(y, y+len(stringList)), stringList): 10 | screen.addstr(yLine, x, line) 11 | 12 | 13 | # Each element of the list goes on a new line, clips long lines, stops at the end 14 | def drawList(y, x, stringList, screen, maxWidth=None, maxHeight=None, boarder=False): 15 | if boarder: 16 | if maxWidth is None: 17 | maxWidth = screen.getmaxyx()[1]-1-x 18 | if maxHeight is None: 19 | maxHeight = screen.getmaxyx()[0]-1 20 | else: 21 | if maxWidth is None: 22 | maxWidth = screen.getmaxyx()[1]-x 23 | if maxHeight is None: 24 | maxHeight = screen.getmaxyx()[0] 25 | 26 | for yLine,line in zip(range(y, maxHeight), stringList): 27 | screen.addstr(yLine, x, line[:maxWidth]) 28 | -------------------------------------------------------------------------------- /components/ProgramState.py: -------------------------------------------------------------------------------- 1 | from components.BinaryNinjaContext import * 2 | from components.MainMenuContext import * 3 | from components.SettingsModule import * 4 | import curses 5 | 6 | 7 | class ProgramState(): 8 | """ A class to hold atributes about the running program """ 9 | 10 | def __init__(self, screen, bv=None): 11 | # Curses Settings 12 | curses.curs_set(False) 13 | screen.nodelay(True) 14 | 15 | self.screen = screen 16 | self.is_running = True 17 | self.settings = SettingsModule() 18 | 19 | if bv is None: 20 | self.context = MainMenuContext(self, screen) 21 | else: 22 | self.context = BinaryNinjaContext(self, screen, bv) 23 | 24 | def __del__(self): 25 | curses.curs_set(True) 26 | 27 | def parseInput(self, key): 28 | # Manage Global KeyBindings First 29 | if key == self.settings["Key_Shutdown"]: 30 | self.shutdown() 31 | 32 | # Secondly Manage Context-Specific Keybindings 33 | self.context.parseInput(key) 34 | 35 | def render(self): 36 | self.context.render() 37 | 38 | def shutdown(self): 39 | self.is_running = False 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kyle Martin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nehebn2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from components import ProgramState 4 | import binaryninja as binja 5 | import argparse 6 | import os.path 7 | import curses 8 | 9 | # TODO...implement live-refreashing the settings.json during run (add the keybinding and check for it here in the global input loop) 10 | # TODO...support multi-key presses? Not sure if this already works or not 11 | # TODO...make sure to support small terminals (I think it does right now, but I should add some more checks so nothing goes out of bounds) 12 | 13 | def main(stdscr): 14 | # Setup 15 | parser = argparse.ArgumentParser(description='Nearly Headless BinaryNinja.') 16 | parser.add_argument('filename', nargs='?', default="") 17 | args = parser.parse_args() 18 | 19 | program = '' 20 | if not args.filename == "": 21 | if os.path.isfile(args.filename): 22 | 23 | bv = binja.BinaryViewType.get_view_of_file(''.join(args.filename), False) 24 | bv.update_analysis() 25 | while not str(bv.analysis_progress) == "Idle": 26 | prog = bv.analysis_progress 27 | 28 | stdscr.erase() 29 | stdscr.border() 30 | 31 | state = '' 32 | if prog.state == binja.AnalysisState.DisassembleState: 33 | state = "Disassembling" 34 | else: 35 | state = "Analyzing" 36 | loadingText = "Loading File: " 37 | 38 | prog = int((prog.count/(prog.total+1))*34.0) 39 | stdscr.addstr(2, 4, loadingText) 40 | stdscr.addstr(2, 4 + len(loadingText), state) 41 | stdscr.addstr(4, 4, '[' + '#'*prog + ' '*(34-prog) + ']') 42 | stdscr.refresh() 43 | program = ProgramState(stdscr, bv) 44 | else: 45 | raise IOError("File does not exist.") 46 | else: 47 | program = ProgramState(stdscr) 48 | 49 | key = "" 50 | while program.is_running: 51 | # Input Filtering, Debuffering, Processing 52 | count = 0 53 | while count < 5: 54 | try: 55 | key = stdscr.getkey() 56 | program.parseInput(key) 57 | count += 1 58 | except curses.error as err: 59 | if not str(err) == "no input": 60 | raise curses.error(str(err)) 61 | else: 62 | key = "" # Clear Key Buffer 63 | break 64 | 65 | # Render 66 | program.render() 67 | curses.doupdate() 68 | 69 | 70 | if __name__ == "__main__": 71 | background = "2a2a2a" 72 | text = "e0e0e0" 73 | curses.wrapper(main) 74 | -------------------------------------------------------------------------------- /components/views/CFGView.py: -------------------------------------------------------------------------------- 1 | from components.utils import drawMultiLineText 2 | from .LeftSideBar import LeftSideBar 3 | from enum import Enum 4 | import curses 5 | 6 | 7 | class CFGView(): 8 | class Focus(Enum): 9 | MAIN = 0 10 | FUNC = 1 11 | XREF = 2 12 | 13 | def __init__(self, bnc): 14 | self.bnc = bnc 15 | 16 | self.focus = CFGView.Focus.MAIN 17 | 18 | self.leftSideBar = LeftSideBar(bnc) 19 | self.cfgScreen = curses.newpad(curses.LINES-1, curses.COLS - self.bnc.program.settings["functionListScreenWidth"]) 20 | 21 | def parseInput_main(self, key): 22 | pass # TODO...implement parseInput_main 23 | 24 | def parseInput(self, key): 25 | if key == self.bnc.program.settings["BinaryNinjaContextSwitchFocus"]: 26 | if self.focus == CFGView.Focus.MAIN: 27 | self.focus = CFGView.Focus.FUNC 28 | elif self.focus == CFGView.Focus.FUNC: 29 | self.focus = CFGView.Focus.XREF 30 | elif self.focus == CFGView.Focus.XREF: 31 | self.focus = CFGView.Focus.MAIN 32 | 33 | if self.bnc.program.settings["BinaryNinjaContextDualFocus"]: 34 | self.parseInput_main(key) 35 | self.leftSideBar.parseInput_functionList(key) 36 | elif self.focus == CFGView.Focus.MAIN: 37 | self.parseInput_main(key) 38 | elif self.focus == CFGView.Focus.FUNC: 39 | self.leftSideBar.parseInput_functionList(key) 40 | elif self.focus == CFGView.Focus.XREF: 41 | self.leftSideBar.parseInput_xrefs(key) 42 | 43 | def render(self): 44 | if self.focus == CFGView.Focus.FUNC: 45 | self.leftSideBar.render(LeftSideBar.Focus.FUNC) 46 | elif self.focus == CFGView.Focus.XREF: 47 | self.leftSideBar.render(LeftSideBar.Focus.XREF) 48 | else: 49 | self.leftSideBar.render(None) 50 | 51 | self.cfgScreen.erase() 52 | if self.focus == CFGView.Focus.MAIN and not self.bnc.program.settings["BinaryNinjaContextDualFocus"]: 53 | self.cfgScreen.border('#', '#', '#', '#', '#', '#', '#', '#') 54 | else: 55 | self.cfgScreen.border() 56 | 57 | # TODO...implement render_cfg (still need to render actual graphs..) 58 | drawMultiLineText(1, 1, "Control Flow Graph!", self.cfgScreen) 59 | title = "CFG" 60 | drawMultiLineText(0, self.cfgScreen.getmaxyx()[1]-len(title)-3, title, self.cfgScreen) 61 | self.cfgScreen.noutrefresh(0, 0, 0, self.bnc.program.settings["functionListScreenWidth"], curses.LINES-2, curses.COLS-1) 62 | 63 | -------------------------------------------------------------------------------- /Logos/medium.logo: -------------------------------------------------------------------------------- 1 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@& #@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2 | @@@@@@@@@@@@@@@@@@@@@@@ .@@@@@@@@@@@@@@@@@@@, @@@@@@@@@@@@@@@@@@@@@@@@@ 3 | @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@. @@@@@@@@@@@@@@@@@@@@@ 4 | @@@@@@@@@@@@@@@@( .@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@, *@@@@@@@@@@@@@@@@ 5 | @@@@@@@@@@@@@@* ,@@@@@@@@@@@@@@%###############&@@@@@@@@@@@@@@, @@@@@@@@@@@@@@ 6 | @@@@@@@@@@@& .@@@@@@@@@@@%#########################%@@@@@@@@@@. @@@@@@@@@@@@@ 7 | @@@@@@@@@& .@@@@@@@@@################################%@@@@@@@@@. @@@@@@@@@@@@ 8 | @@@@@@@@* .@@@@@@@@######################################&@@@@@@@@. @@@@@@@@@ 9 | @@@@@@( ,@@@@@@@@#############################################@@@@@@@. @@@@@@ 10 | @@@@@, @@@@@@@@################################################%@@@@@@@ @@@@@ 11 | @@@@. @@@@@@@@###################################################@@@@@@@ @@@@ 12 | @@@. @@@@@@@####################################################%@@@@@@@@ @@@ 13 | @@, @@@@@@@%#######################################################%@@@@@@. @@ 14 | @/ @@@@@@@%#########################################################%@@@@@@. ,@ 15 | @ .@@@@@@##########################################################&@@@@@@@@ 16 | * @@@@@@@#############################################################@@@@@@& 17 | ,@@@@@@%####%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#####@@@@@@. 18 | @@@@@@@#####%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#####@@@@@@@ 19 | @@@@@@@#####%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#####&@@@@@@ 20 | @@@@@@####%@@@@@&.......................................,@@@@@@#####@@@@@@@@@ 21 | @@@@@@####%@@@@@& .@@@@@@#####@@@@@@@@ 22 | @@@@@@@#####%@@@@@& /@@@@@%. .@@@@@@#####@@@@@@@ 23 | @@@@@@@######@@@@@@ @@@@@@@@@( *@@@@@@#####@@@@@@@ 24 | .@@@@@@%#####@@@@@@@/ ,%@@@@@@@@@@@@. ,&@@@@@@####@@@@@@@@. 25 | . @@@@@@@######@@@@@@@@@@@@@@@@@@@@@@@@##@@@@@@@@@@@@@@@@@@@@@@@@#####@@@@@@@ 26 | @ @@@@@@@#####%@@@@@@@@@@@@@@@@@@@@@###@@@@@@@@@@@@@@@@@@@@@##########@@@@@ @ 27 | @ @@@@@@@######@@@@@@@@@@@@@@@@@%########&@@@@@@@@@@@@@@@@%########@@@@@@( @ 28 | @@ @@@@@@@###########((((((###################(((((((#############@@@@@@& @@ 29 | @@@ @@@@@@@#######################################################@@@@@& @@@ 30 | @@@@ @@@@@@@@###################################################@@@@@@@% @@@@ 31 | @@@@@ @@@@@@@@%################################################@@@@@@@/ @@@@@ 32 | @@@@@@ @@@@@@@@%############################################@@@@@@@@ @@@@@@@ 33 | @@@@@@@@ @@@@@@@@@#######################################@@@@@@@@@. @@@@@@@@ 34 | @@@@@@@@@@ @@@@@@@@@@#################################&@@@@@@@@@, @@@@@@@@@@ 35 | @@@@@@@@@@@@ @@@@@@@@@@@########################%@@@@@@@@@@@. @@@@@@@@@@@@ 36 | @@@@@@@@@@@@@@ @@@@@@@@@@@@@@%#############%@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ 37 | @@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@ 38 | @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@ 39 | @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@ -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Nehebn2 2 | NEarlyHEadlessBinaryNinja(2) 3 | 4 | The only thing better than completely headless. 5 | 6 | --- 7 | 8 | (TL;DR at bottom) 9 | 10 | A friend of mine threatened to use R2 so he wouldn't have to use a GUI, and on my honor as Binary Ninja in training (aka summer intern) I felt duty-bound to set out upon this mission... A couple weeks of my free time and roughly 800 lines of python3 later, we have this: a CLI for BinaryNinja: N2. 11 | 12 | Though, any more of my free time would be kinda ridiculous for a private little gag, so I'm open sourcing N2...Most of the initial grunt-work is now complete, and we have: 13 | 14 | - Linear Disassembly View, with scroll up/down AND page up/down. WOW! 15 | - 6.5 xrefs! WOAH! 16 | - A scrollable list of functions, and the ability to jump to one at will, KAACHOW!! 17 | - Hex View with no visible horizontal cursor! Count me in! 18 | - A place holder for Control Flow Graphs! POW! 19 | - A main menu! Ding ding ding! 20 | - And _customizable key-bindings_ KABLAM! 21 | (just go ahead and mentally render that like a comic book for me please) 22 | 23 | Currently, if you didn't get this from the list above, N2 is seriously lacking... But I made sure to keep track and make a list of the most important things that need to be added, and if you're interested a quick `grep -r "# TODO..."` gives us: 24 | 25 | ``` 26 | # TODO...FIX the ugly main menu screen...seriously 27 | # TODO...implement color themes with the new color theme format courtesy of peter 28 | # TODO...implement disassembly options 29 | # TODO...implement scroll bars on windows?, and the setting to disable them 30 | # TODO...implement patching linear/cfg 31 | # TODO...implement the ability to cursor into linear assembly lines. 32 | # TODO...implement undo! I think the API actually takes care of this for us nicely, but editing has to be implemented first and it'd be nice to snap the view back to whatever is being edited too 33 | # TODO...Implement Wrap Around 34 | # TODO...Implement Not Crashing On Moving Past Buffer 35 | # TODO...implement xrefs window up/down, cursor, and selection mechanics 36 | # TODO...FIX...stop clipping 37 | # TODO...FIX...will currently jump to the top of a block, also skips down in some cases? 38 | # TODO...make this for loop at least reasonably efficient...it's seriously just a clusterfuck right now 39 | # TODO...implement parseInput_cfg_main 40 | # TODO...FIX the lag induced by fetching the names of the functions (seems to take forever) 41 | # TODO...get and render xrefs in a more elegant manner 42 | # TODO...implement render_cfg (still need to render actual graphs..) 43 | # TODO...seperate views into different functions 44 | # TODO...add some settings to the settings view 45 | # TODO...implement live-refreshing the settings.json during run (add the key-binding and check for it here in the global input loop) 46 | # TODO...support multi-key presses? Not sure if this already works or not 47 | ``` 48 | 49 | As you can see, there is plenty left to do and I am eager to do them, but I want to know what you think!: 50 | 51 | ![Demo](demo.gif) 52 | 53 | To be clear, this is built using the [BinaryNinjaAPI](github.com/Vector35/binaryninja-api) and python3. To use this, you will need to have a [headless license from Vector35](binary.ninja/purchase/)...though you might also be able to run it in the script console as well. 54 | 55 | TL;DR: 56 | If you're interested in this thumbs up on [GitHub issue #1](github.com/KyleMiles/nehebn2/issues/1) and if there's enough interest I'll keep working on this rather than moving on to another project. If you want to contribute, I'll be happy to accept reasonably well-done pull requests. 57 | 58 | I hope you like what you see, and I hope you let me know so I can keep working on this...but until then, try to keep your head on straight! 59 | -------------------------------------------------------------------------------- /components/MainMenuContext.py: -------------------------------------------------------------------------------- 1 | from components.BinaryNinjaContext import * 2 | from components.utils import * 3 | import binaryninja as binja 4 | import pyfiglet 5 | import curses 6 | import os 7 | 8 | 9 | # TODO...FIX the ugly main menu screen...seriously 10 | 11 | 12 | class MainMenuContext(): 13 | def __init__(self, program, screen): 14 | self.program = program 15 | self.screen = screen 16 | self.welcome = pyfiglet.figlet_format('Welcome to,\n Binary\n Ninja', font='rowancap', width=curses.COLS) 17 | self.bnlogo = ''.join(open("Logos/small.logo").readlines()) 18 | 19 | self.menupad = None 20 | self.menuContext = 0 21 | # 1 = Open 22 | # 2 = Options 23 | 24 | def parseInput(self, key): 25 | if self.menuContext == 0: # Main screen 26 | if key == 'o' or key == 'O': # Open File Menu 27 | self.menupad = None 28 | self.menuContext = 1 29 | if key == 'e' or key == 'E': # Open File Menu 30 | self.menupad = None 31 | self.menuContext = 2 32 | 33 | elif self.menuContext == 1: # Open File Menu 34 | if key == "KEY_F(2)": # Main screen 35 | del self.filename 36 | self.menupad = None 37 | self.menuContext = 0 38 | else: 39 | if key == "KEY_BACKSPACE": 40 | if self.cursorIndex > 0: 41 | self.cursorIndex -= 1 42 | self.filename[self.cursorIndex] = "_" 43 | 44 | elif key == "\n": # LOAD 45 | if os.path.isfile(''.join(self.filename[:self.cursorIndex])): 46 | # Load the actual program 47 | bv = binja.BinaryViewType.get_view_of_file(''.join(self.filename[:self.cursorIndex]), False) 48 | bv.update_analysis() 49 | while not str(bv.analysis_progress) == "Idle": 50 | prog = bv.analysis_progress 51 | 52 | self.menupad.erase() 53 | self.menupad.border() 54 | 55 | state = '' 56 | if prog.state == binja.AnalysisState.DisassembleState: 57 | state = "Disassembling" 58 | else: 59 | state = "Analyzing" 60 | loadingText = "Loading File" 61 | 62 | prog = int((prog.count/(prog.total+1))*34.0) 63 | self.menupad.addstr(1, int(2+(36/2)-(len(loadingText)/2)), loadingText) 64 | self.menupad.addstr(3, int(2+(36/2)-(len(state)/2)), state) 65 | self.menupad.addstr(4, 2, '[' + '#'*prog + ' '*(34-prog) + ']') 66 | self.menupad.refresh(0, 0, int(curses.LINES/2)-3, int(curses.COLS/2)-20, int(curses.LINES/2)+4, int(curses.COLS/2)+20) 67 | self.program.context = BinaryNinjaContext(self.program, self.screen, bv) 68 | self.program.context.parseInput(None) 69 | 70 | else: 71 | self.filename = [i for i in "File Does Not Exist_________________"] 72 | self.cursorIndex = 0 73 | 74 | elif not key == '': # `User is Typing...`` 75 | if self.cursorIndex < len(self.filename): 76 | self.filename[self.cursorIndex] = key 77 | self.cursorIndex += 1 78 | # Clear the rest of the string 79 | for i in range(self.cursorIndex, len(self.filename)): 80 | self.filename[i] = '_' 81 | 82 | elif self.menuContext == 2: # Edit Option 83 | if key == "KEY_F(2)": # Main screen 84 | self.menupad = None 85 | self.menuContext = 0 86 | 87 | def render(self): 88 | # TODO...seperate views into different functions 89 | # TODO...add some settings to the settings view 90 | self.screen.erase() 91 | 92 | self.screen.border() 93 | drawMultiLineText(1, 2, self.bnlogo, self.screen) 94 | drawMultiLineText(2, 44, self.welcome, self.screen) 95 | self.screen.addstr(20, 100, "UI by Elyk") 96 | 97 | self.screen.noutrefresh() 98 | 99 | if self.menuContext == 0: # Main Menu 100 | if not self.menupad: 101 | self.menupad = curses.newpad(7, 22) 102 | self.menupad.erase() 103 | self.menupad.border() 104 | self.menupad.addstr(2, 2, "'O' : Open File") 105 | self.menupad.addstr(4, 2, "'E' : Edit Options") 106 | self.menupad.noutrefresh(0, 0, int(curses.LINES/2)-3, int(curses.COLS/2)-11, int(curses.LINES/2)+4, int(curses.COLS/2)+11) 107 | 108 | elif self.menuContext == 1: # Open 109 | if not self.menupad: # Just entering context 110 | self.menupad = curses.newpad(7, 40) 111 | self.filename = ["_"] * 36 112 | self.cursorIndex = 0 113 | 114 | self.menupad.erase() 115 | self.menupad.border() 116 | 117 | self.menupad.addstr(2, 2, "Filename: ('F2' : Back)") 118 | self.menupad.addstr(4, 2, ''.join(self.filename)) 119 | self.menupad.noutrefresh(0, 0, int(curses.LINES/2)-3, int(curses.COLS/2)-20, int(curses.LINES/2)+4, int(curses.COLS/2)+20) 120 | 121 | elif self.menuContext == 2: # Options 122 | self.menupad = curses.newpad(30, 40) 123 | 124 | self.menupad.erase() 125 | self.menupad.border() 126 | 127 | self.menupad.addstr(2, 2, "Options: ('F2' : Back)") 128 | self.menupad.noutrefresh(0, 0, int(curses.LINES/2)-7, int(curses.COLS/2)-20, int(curses.LINES/2)+23, int(curses.COLS/2)+20) 129 | -------------------------------------------------------------------------------- /components/views/LeftSideBar.py: -------------------------------------------------------------------------------- 1 | from components.utils import drawList, drawMultiLineText 2 | from enum import Enum 3 | import curses 4 | 5 | 6 | class LeftSideBar(): 7 | class Focus(Enum): 8 | FUNC = 1 9 | XREF = 2 10 | 11 | def __init__(self, bnc): 12 | self.bnc = bnc 13 | 14 | self.functionListScreen = curses.newpad(curses.LINES-1-self.bnc.program.settings["xrefsScreenHeight"], self.bnc.program.settings["functionListScreenWidth"]) 15 | self.xrefsScreen = curses.newpad(self.bnc.program.settings["xrefsScreenHeight"], self.bnc.program.settings["functionListScreenWidth"]) 16 | 17 | # Function List Pane Stuff 18 | self.funcListPos = 0 19 | self.funcListCur = 0 20 | self.updateFunctionList = True 21 | 22 | def render_functionList(self, focus): 23 | # Not the best implementation of this...but it makes only the functionList window input-lag (unless in dual focus mode... :/ ) 24 | # TODO...FIX the lag induced by fetching the names of the functions (seems to take forever) 25 | if self.updateFunctionList: 26 | self.functionListScreen.erase() 27 | 28 | if focus == LeftSideBar.Focus.FUNC and not self.bnc.program.settings["BinaryNinjaContextDualFocus"]: 29 | self.functionListScreen.border('#', '#', '#', '#', '#', '#', '#', '#') 30 | else: 31 | self.functionListScreen.border() 32 | 33 | if self.updateFunctionList: 34 | for yLine in range(self.funcListPos, self.funcListPos+(curses.LINES-3)-self.bnc.program.settings["xrefsScreenHeight"]): 35 | if yLine == self.funcListCur + self.funcListPos: 36 | self.functionListScreen.addstr(yLine-self.funcListPos+1, 3, self.bnc.bv.functions[yLine].name, curses.A_STANDOUT) # Name access is slow 37 | else: 38 | self.functionListScreen.addstr(yLine-self.funcListPos+1, 3, self.bnc.bv.functions[yLine].name) # Name access is slow 39 | self.updateFunctionList = False 40 | 41 | title = "Function List" 42 | drawMultiLineText(0, self.bnc.program.settings["functionListScreenWidth"]-len(title)-2, title, self.functionListScreen) 43 | self.functionListScreen.noutrefresh(0, 0, 0, 0, curses.LINES-2-self.bnc.program.settings["xrefsScreenHeight"], self.bnc.program.settings["functionListScreenWidth"]) 44 | 45 | def render_xrefs(self, focus): 46 | self.xrefsScreen.erase() 47 | if focus == LeftSideBar.Focus.XREF and not self.bnc.program.settings["BinaryNinjaContextDualFocus"]: 48 | self.xrefsScreen.border('#', '#', '#', '#', '#', '#', '#', '#') 49 | else: 50 | self.xrefsScreen.border() 51 | 52 | # TODO...get and render xrefs in a more elegant manner 53 | 54 | xrefs = self.bnc.bv.get_code_refs(self.bnc.pos) 55 | xrefLines = [] 56 | for yLine, xref in zip(range(len(xrefs)), xrefs): 57 | line = "{:08x}".format(xref.address) 58 | line += " in " + xref.function.name 59 | xrefLines.append(line) 60 | line = " " + self.bnc.bv.get_disassembly(xref.address) 61 | xrefLines.append(line) 62 | drawList(1, 2, xrefLines, self.xrefsScreen, boarder=True) 63 | 64 | if len(xrefLines) == 0: 65 | self.xrefsScreen.addstr(1, 2, ":(") 66 | 67 | title = "XREFS" 68 | drawMultiLineText(0, self.bnc.program.settings["functionListScreenWidth"]-len(title)-2, title, self.xrefsScreen) 69 | self.xrefsScreen.noutrefresh(0, 0, curses.LINES-1-self.bnc.program.settings["xrefsScreenHeight"], 0, curses.LINES-2, self.bnc.program.settings["functionListScreenWidth"]) 70 | 71 | def render(self, focus): 72 | self.render_functionList(focus) 73 | self.render_xrefs(focus) 74 | 75 | def parseInput_functionList(self, key): 76 | bvFunctionsLen = len(self.bnc.bv.functions) 77 | if key == self.bnc.program.settings["functionListScrollDown"]: 78 | self.funcListCur += 1 79 | elif key == self.bnc.program.settings["functionListScrollUp"]: 80 | self.funcListCur -= 1 81 | elif key == self.bnc.program.settings["functionListPageDown"]: 82 | self.funcListCur += self.functionListScreen.getmaxyx()[0]-3 83 | elif key == self.bnc.program.settings["functionListPageUp"]: 84 | self.funcListCur -= self.functionListScreen.getmaxyx()[0]-3 85 | elif key == self.bnc.program.settings["functionListSelect"]: 86 | selection = self.bnc.bv.functions[self.funcListPos + self.funcListCur] 87 | self.bnc.pos= selection.start 88 | self.cursorOffset = 0 89 | self.loadLinearDisassembly() 90 | 91 | if self.funcListCur > self.functionListScreen.getmaxyx()[0]-3: 92 | self.funcListPos += self.funcListCur - self.functionListScreen.getmaxyx()[0]+3 93 | self.funcListCur = self.functionListScreen.getmaxyx()[0]-3 94 | elif self.funcListCur < 0: 95 | self.funcListPos += self.funcListCur 96 | self.funcListCur = 0 97 | 98 | if self.funcListPos < 0: 99 | self.funcListPos = 0 100 | elif self.funcListPos > bvFunctionsLen - self.functionListScreen.getmaxyx()[0]+2: 101 | self.funcListPos = bvFunctionsLen - self.functionListScreen.getmaxyx()[0]+2 102 | 103 | # TODO...Implement Wrap Around 104 | # TODO...Implement Not Crashing On Moving Past Buffer 105 | 106 | self.updateFunctionList = True 107 | 108 | def parseInput_xrefs(self, key): 109 | # TODO...implement xrefs window up/down, cursor, and selection mechanics 110 | pass 111 | -------------------------------------------------------------------------------- /components/views/LinearView.py: -------------------------------------------------------------------------------- 1 | from components.utils import drawMultiLineText 2 | from .LeftSideBar import LeftSideBar 3 | from enum import Enum 4 | import curses 5 | 6 | 7 | class LinearView(): 8 | class Focus(Enum): 9 | MAIN = 0 10 | FUNC = 1 11 | XREF = 2 12 | 13 | def __init__(self, bnc): 14 | self.bnc = bnc 15 | 16 | self.focus = LinearView.Focus.MAIN 17 | self.posOffset = 0 # Displacement in lines between the top of the screen and the rendered location of self.pos 18 | self.cursorOffset = 0 # Offset (in lines) from the top of the screen to the current visual cursor in linear view 19 | self.disassemblyLines = [] 20 | 21 | self.leftSideBar = LeftSideBar(bnc) 22 | self.linearDisassemblyScreen = curses.newpad(curses.LINES-1, curses.COLS - self.bnc.program.settings["functionListScreenWidth"]) 23 | self.loadLinearDisassembly() 24 | 25 | def loadLinearDisassembly(self): 26 | # Get current offset into block 27 | disassBlockOffset = 0 28 | for line in self.bnc.bv.get_next_linear_disassembly_lines(self.bnc.bv.get_linear_disassembly_position_at(self.bnc.pos, self.bnc.disassemblySettings), self.bnc.disassemblySettings): 29 | if line.contents.address == self.bnc.pos: 30 | break 31 | else: 32 | disassBlockOffset += 1 33 | 34 | # Displacement in lines between the top of the screen and the current disassembly block's top 35 | realOffset = self.posOffset - disassBlockOffset 36 | self.realOffset = realOffset 37 | # Generate two cursors, one for loading up one for loading down 38 | curPosU = self.bnc.bv.get_linear_disassembly_position_at(self.bnc.pos, self.bnc.disassemblySettings) # Get linear disassembly position 39 | topLines = [] 40 | while len(topLines) < realOffset: 41 | newLines = self.bnc.bv.get_previous_linear_disassembly_lines(curPosU, self.bnc.disassemblySettings) 42 | if len(newLines) == 0: 43 | break 44 | else: 45 | topLines = newLines + topLines 46 | 47 | curPosD = self.bnc.bv.get_linear_disassembly_position_at(self.bnc.pos, self.bnc.disassemblySettings) # Get linear disassembly position 48 | bottomLines = [] 49 | while len(bottomLines) <= (self.linearDisassemblyScreen.getmaxyx()[0]-2) - realOffset: 50 | newLines = self.bnc.bv.get_next_linear_disassembly_lines(curPosD, self.bnc.disassemblySettings) 51 | if len(newLines) == 0: 52 | break 53 | else: 54 | bottomLines += newLines 55 | 56 | self.disassemblyLines = topLines + bottomLines 57 | if realOffset < 0: 58 | self.disassemblyLines = self.disassemblyLines[-1*realOffset:] 59 | else: 60 | self.disassemblyLines = self.disassemblyLines[len(topLines)-realOffset:] 61 | 62 | def parseInput_main(self, key): 63 | # TODO...FIX...stop clipping 64 | 65 | # Scroll 66 | if key == self.bnc.program.settings["linearDisassemblyScrollDown"]: 67 | self.cursorOffset += 1 68 | elif key == self.bnc.program.settings["linearDisassemblyScrollUp"]: 69 | self.cursorOffset -= 1 70 | elif key == self.bnc.program.settings["linearDisassemblyPageDown"]: 71 | self.cursorOffset += self.linearDisassemblyScreen.getmaxyx()[0]-3 72 | elif key == self.bnc.program.settings["linearDisassemblyPageUp"]: 73 | self.cursorOffset -= self.linearDisassemblyScreen.getmaxyx()[0]-3 74 | 75 | # Adjust for off screen 76 | if self.cursorOffset < 0: 77 | self.posOffset -= self.cursorOffset 78 | self.cursorOffset = 0 79 | self.loadLinearDisassembly() 80 | elif self.cursorOffset > self.linearDisassemblyScreen.getmaxyx()[0]-3: 81 | self.posOffset -= self.cursorOffset - (self.linearDisassemblyScreen.getmaxyx()[0]-3) 82 | self.cursorOffset = self.linearDisassemblyScreen.getmaxyx()[0]-3 83 | self.loadLinearDisassembly() 84 | 85 | # Adjust for new address 86 | if self.disassemblyLines[self.cursorOffset].contents.address != self.bnc.pos: 87 | self.bnc.pos = self.disassemblyLines[self.cursorOffset].contents.address 88 | self.posOffset = self.cursorOffset # TODO...FIX...will currently jump to the top of a block, also skips down in some cases? 89 | self.loadLinearDisassembly() 90 | 91 | def parseInput(self, key): 92 | if key == self.bnc.program.settings["BinaryNinjaContextSwitchFocus"]: 93 | if self.focus == LinearView.Focus.MAIN: 94 | self.focus = LinearView.Focus.FUNC 95 | elif self.focus == LinearView.Focus.FUNC: 96 | self.focus = LinearView.Focus.XREF 97 | elif self.focus == LinearView.Focus.XREF: 98 | self.focus = LinearView.Focus.MAIN 99 | 100 | if self.bnc.program.settings["BinaryNinjaContextDualFocus"]: 101 | self.leftSideBar.parseInput_functionList(key) 102 | self.parseInput_main(key) 103 | elif self.focus == LinearView.Focus.MAIN: 104 | self.parseInput_main(key) 105 | elif self.focus == LinearView.Focus.FUNC: 106 | self.leftSideBar.parseInput_functionList(key) 107 | elif self.focus == LinearView.Focus.XREF: 108 | self.leftSideBar.parseInput_xrefs(key) 109 | 110 | def render(self): 111 | if self.focus == LinearView.Focus.FUNC: 112 | self.leftSideBar.render(LeftSideBar.Focus.FUNC) 113 | elif self.focus == LinearView.Focus.XREF: 114 | self.leftSideBar.render(LeftSideBar.Focus.XREF) 115 | else: 116 | self.leftSideBar.render(None) 117 | 118 | self.linearDisassemblyScreen.erase() 119 | if self.focus == LinearView.Focus.MAIN and not self.bnc.program.settings["BinaryNinjaContextDualFocus"]: 120 | self.linearDisassemblyScreen.border('#', '#', '#', '#', '#', '#', '#', '#') 121 | else: 122 | self.linearDisassemblyScreen.border() 123 | 124 | rendRange = 0 125 | if len(self.disassemblyLines) < self.linearDisassemblyScreen.getmaxyx()[0]-2: 126 | rendRange = range(len(self.disassemblyLines)) 127 | else: 128 | rendRange = range(self.linearDisassemblyScreen.getmaxyx()[0]-2) 129 | 130 | for yLine, textLine in zip(rendRange, self.disassemblyLines): 131 | if yLine == self.cursorOffset: 132 | self.linearDisassemblyScreen.addstr(yLine+1, 2, str(textLine), curses.A_STANDOUT) 133 | else: 134 | self.linearDisassemblyScreen.addstr(yLine+1, 2, str(textLine)) 135 | 136 | title = "Linear" 137 | drawMultiLineText(0, self.linearDisassemblyScreen.getmaxyx()[1]-len(title)-3, title, self.linearDisassemblyScreen) 138 | self.linearDisassemblyScreen.noutrefresh(0, 0, 0, self.bnc.program.settings["functionListScreenWidth"], curses.LINES-2, curses.COLS-1) 139 | -------------------------------------------------------------------------------- /components/BinaryNinjaContext.py: -------------------------------------------------------------------------------- 1 | from components.utils import drawMultiLineText 2 | from binaryninja import DisassemblySettings 3 | # from .views import LinearView.LinearView, HexView.HexView, CFGView.CFGView 4 | from .views.LinearView import LinearView 5 | from .views.HexView import HexView 6 | from .views.CFGView import CFGView 7 | from enum import Enum 8 | import curses 9 | 10 | 11 | # TODO...Loosely ordered by importance: 12 | # TODO...implement color themes with the new color theme format courtesy of peter 13 | # TODO...implement disassembly options 14 | # TODO...implement scroll bars on windows?, and the setting to disable them 15 | # TODO...implement patching linear/cfg 16 | # TODO...implement the ability to cursor into linear assembly lines. 17 | # TODO...implement undo! I think the API actually takes care of this for us nicely, but editing has to be implemented first and it'd be nice to snap the view back to whatever is being edited too 18 | 19 | 20 | class BinaryNinjaContext(): 21 | """ The BinaryNinja UI """ 22 | 23 | class View(Enum): 24 | LINEAR = 0 25 | HEX = 1 26 | CFG = 2 27 | 28 | class Popup(Enum): 29 | NONE = 0 30 | GOTO = 1 31 | 32 | def __init__(self, program, screen, bv): 33 | self.program = program 34 | self.screen = screen 35 | self.bv = bv 36 | 37 | # Make Pads 38 | self.alertsScreen = curses.newpad(1, curses.COLS) 39 | self.pythonConsoleScreen = curses.newpad(1, curses.COLS) 40 | self.logScreen = curses.newpad(1, curses.COLS) 41 | self.popupGotoScreen = curses.newpad(3, 16) 42 | 43 | # Disassembly Window Stuff 44 | self.disassemblySettings = DisassemblySettings() 45 | 46 | self.pos = self.bv.start # Current position in the binary 47 | 48 | # Global Stuff 49 | self.view = BinaryNinjaContext.View.LINEAR 50 | self.popup = BinaryNinjaContext.Popup.NONE 51 | 52 | # Set up first view 53 | self.currentView = LinearView(self) 54 | 55 | def parseInput_popup_goto(self, key): 56 | if key == "KEY_BACKSPACE": 57 | if self.gotoCursor > 0: 58 | self.gotoCursor -= 1 59 | self.gotoInput[self.gotoCursor] = "_" 60 | else: 61 | self.popup = BinaryNinjaContext.Popup.NONE 62 | del self.gotoInput 63 | del self.gotoCursor 64 | 65 | elif key == self.program.settings["confirm"]: 66 | try: 67 | gotoInput = eval(''.join(self.gotoInput[:self.gotoCursor])) 68 | if isinstance(gotoInput, int): 69 | 70 | if self.view == BinaryNinjaContext.View.LINEAR: 71 | self.pos = gotoInput 72 | self.currentView.cursorOffset = 0 73 | self.currentView.loadLinearDisassembly() 74 | elif self.view == BinaryNinjaContext.View.HEX: 75 | self.pos = gotoInput 76 | self.currentView.hexOffset = 0 77 | self.currentView.hexCursor = self.pos 78 | self.currentView.loadHexLines() 79 | elif self.view == BinaryNinjaContext.View.CFG: 80 | self.pos = gotoInput 81 | 82 | self.popup = BinaryNinjaContext.Popup.NONE 83 | del self.gotoInput 84 | del self.gotoCursor 85 | except: 86 | pass 87 | 88 | elif key == "KEY_LEFT": 89 | if self.gotoCursor > 0: 90 | self.gotoCursor -= 1 91 | elif key == "KEY_RIGHT": 92 | if self.gotoCursor < len(self.gotoInput): 93 | self.gotoCursor -= 1 94 | 95 | elif not key == '': # `User is Typing...`` 96 | if self.gotoCursor < len(self.gotoInput): 97 | self.gotoInput[self.gotoCursor] = key 98 | self.gotoCursor += 1 99 | # Clear the rest of the string 100 | for i in range(self.gotoCursor, len(self.gotoInput)): 101 | self.gotoInput[i] = '_' 102 | 103 | def parseInput(self, key): 104 | if key == self.program.settings["BinaryNinjaContextSwitchView"]: 105 | # Clean Up 106 | del self.currentView 107 | 108 | # Switch Contexts 109 | if self.view == BinaryNinjaContext.View.LINEAR: 110 | self.view = BinaryNinjaContext.View.HEX 111 | elif self.view == BinaryNinjaContext.View.HEX: 112 | self.view = BinaryNinjaContext.View.CFG 113 | elif self.view == BinaryNinjaContext.View.CFG: 114 | self.view = BinaryNinjaContext.View.LINEAR 115 | 116 | # Setup 117 | if self.view == BinaryNinjaContext.View.LINEAR: 118 | self.currentView = LinearView(self) 119 | elif self.view == BinaryNinjaContext.View.HEX: 120 | self.currentView = HexView(self) 121 | elif self.view == BinaryNinjaContext.View.CFG: 122 | self.currentView = CFGView(self) 123 | 124 | # Popup context switches 125 | elif (key == self.program.settings["popupWindowGoto"].lower() or key == self.program.settings["popupWindowGoto"].upper()) and self.popup == BinaryNinjaContext.Popup.NONE: 126 | self.popup = BinaryNinjaContext.Popup.GOTO 127 | self.gotoInput = ['_'] * 12 128 | self.gotoCursor = 0 129 | 130 | # Popup input capturing 131 | elif self.popup == BinaryNinjaContext.Popup.GOTO: 132 | self.parseInput_popup_goto(key) 133 | 134 | # Views input capturing 135 | else: 136 | self.currentView.parseInput(key) 137 | 138 | def render_alerts(self): 139 | self.alertsScreen.erase() 140 | 141 | alerts = "" 142 | alerts += str(self.bv) + " " 143 | alerts += "Cursor Position: " + hex(self.pos) + " " 144 | try: 145 | # alerts += "Disass Lines: " + str(len(self.disassemblyLines)) + " " 146 | alerts += "PosOffset: " + str(self.posOffset) + " " 147 | # alerts += "LinDisCursOffset: " + str(self.cursorOffset) + " " 148 | # alerts += "realOff: " + str(self.realOffset) + " " 149 | alerts += "hexCursor: " + str(self.hexCursor) + " " 150 | alerts += "xthing: " + str(self.xthing) + " " 151 | # alerts += "" + str() + " " 152 | except: 153 | pass 154 | self.alertsScreen.addstr(0, 0, alerts) 155 | self.alertsScreen.noutrefresh(0, 0, curses.LINES-1, 0, curses.LINES-1, curses.COLS-1) 156 | 157 | def render_popups(self): 158 | if self.popup == BinaryNinjaContext.Popup.NONE: 159 | return 160 | 161 | if self.popup == BinaryNinjaContext.Popup.GOTO: 162 | self.popupGotoScreen.erase() 163 | self.popupGotoScreen.border() 164 | 165 | drawMultiLineText(0, 10, "Goto", self.popupGotoScreen) 166 | drawMultiLineText(1, 2, ''.join(self.gotoInput), self.popupGotoScreen) 167 | self.popupGotoScreen.noutrefresh(0, 0, (curses.LINES//2)-1, (curses.COLS//2)-8, (curses.LINES//2)+2, (curses.COLS//2)+8) 168 | 169 | def render(self): 170 | self.currentView.render() 171 | self.render_alerts() 172 | self.render_popups() 173 | -------------------------------------------------------------------------------- /components/views/HexView.py: -------------------------------------------------------------------------------- 1 | from components.utils import drawMultiLineText 2 | from binaryninja import BinaryReader 3 | import curses 4 | 5 | 6 | class HexView(): 7 | 8 | def __init__(self, bnc): 9 | self.bnc = bnc 10 | 11 | self.br = BinaryReader(bnc.bv) 12 | 13 | self.hexScreen = curses.newpad(curses.LINES-1, curses.COLS) 14 | self.hexOffset = 0 15 | self.hexCursor = bnc.pos 16 | self.loadHexLines() 17 | 18 | def loadHexLines(self): 19 | # Get lines to render 20 | self.hexLines = [] 21 | topOfScreen = (self.bnc.pos- (self.bnc.pos% self.bnc.program.settings["hexLineLength"])) - (self.hexOffset * self.bnc.program.settings["hexLineLength"]) 22 | self.topOfScreen = topOfScreen 23 | self.br.seek(topOfScreen) 24 | # TODO...make this for loop at least reasonably efficient...it's seriously just a clusterfuck right now 25 | for _ in range(self.hexScreen.getmaxyx()[0]-2): 26 | offset = "{:08x} ".format(self.br.offset) 27 | line_bytes = self.br.read(self.bnc.program.settings["hexLineLength"]) 28 | 29 | # Sections that don't exist in the memory 30 | if line_bytes is None: 31 | line_bytes = b'' 32 | line_byte = self.br.read(1) 33 | while line_byte is not None: 34 | line_bytes += line_byte 35 | line_byte = self.br.read(1) 36 | 37 | if len(line_bytes) == self.bnc.program.settings["hexLineLength"]: 38 | byteValues = ''.join(["{:02x} ".format(b) for b in line_bytes])[:-1] 39 | asciiValues = ''.join([chr(int(b, 16)) if (int(b, 16) > 31 and int(b, 16) < 127) else '.' for b in byteValues.split(' ')]) 40 | self.hexLines.append(offset + byteValues + " " + asciiValues) 41 | 42 | else: 43 | byteValues = ''.join(["{:02x} ".format(b) for b in line_bytes])[:-1] 44 | asciiValues = ''.join([chr(int(b, 16)) if (int(b, 16) > 31 and int(b, 16) < 127) else '.' for b in byteValues.split(' ')[:-1]]) 45 | self.hexLines.append(offset + byteValues + " "*(self.bnc.program.settings["hexLineLength"]*3-len(byteValues)) + " " + asciiValues) 46 | if (len(self.hexLines) != self.hexScreen.getmaxyx()[0]-2): 47 | self.hexLines.append('-'*(self.bnc.program.settings["hexLineLength"]*4 + 13)) 48 | 49 | line_byte = None 50 | while line_byte is None and self.br.offset <= self.bnc.bv.end: 51 | self.br.seek(self.br.offset+1) 52 | line_byte = self.br.read(1) 53 | self.br.seek(self.br.offset-1) 54 | 55 | if (len(self.hexLines) == self.hexScreen.getmaxyx()[0]-2): 56 | break 57 | 58 | def parseInput(self, key): 59 | # Scroll 60 | if key == self.bnc.program.settings["hexViewRight"]: 61 | self.hexCursor += 0.5 62 | 63 | if self.hexCursor%1.0 == 0: 64 | self.bnc.pos+= 1 65 | if self.bnc.pos% self.bnc.program.settings["hexLineLength"] == 0: 66 | self.hexOffset += 1 67 | 68 | elif key == self.bnc.program.settings["hexViewLeft"]: 69 | self.hexCursor -= 0.5 70 | 71 | if self.hexCursor%1.0 == 0.5: 72 | if self.bnc.pos% self.bnc.program.settings["hexLineLength"] == 0: 73 | self.hexOffset -= 1 74 | self.bnc.pos-= 1 75 | 76 | elif key == self.bnc.program.settings["hexViewLineDown"]: 77 | self.hexOffset += 1 78 | self.bnc.pos+= self.bnc.program.settings["hexLineLength"] 79 | self.hexCursor += self.bnc.program.settings["hexLineLength"] 80 | elif key == self.bnc.program.settings["hexViewLineUp"]: 81 | self.hexOffset -= 1 82 | self.bnc.pos-= self.bnc.program.settings["hexLineLength"] 83 | self.hexCursor -= self.bnc.program.settings["hexLineLength"] 84 | elif key == self.bnc.program.settings["hexViewPageDown"]: 85 | self.hexOffset += self.hexScreen.getmaxyx()[0]-3 86 | self.bnc.pos+= self.bnc.program.settings["hexLineLength"] * (self.hexScreen.getmaxyx()[0]-3) 87 | self.hexCursor += self.bnc.program.settings["hexLineLength"] * (self.hexScreen.getmaxyx()[0]-3) 88 | elif key == self.bnc.program.settings["hexViewPageUp"]: 89 | self.hexOffset -= self.hexScreen.getmaxyx()[0]-3 90 | self.bnc.pos-= self.bnc.program.settings["hexLineLength"] * (self.hexScreen.getmaxyx()[0]-3) 91 | self.hexCursor -= self.bnc.program.settings["hexLineLength"] * (self.hexScreen.getmaxyx()[0]-3) 92 | 93 | elif key in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']: 94 | if self.hexCursor % 1.0 == 0.5: 95 | self.bnc.bv.write(self.bnc.pos, (int(key, 16) | 96 | (ord(self.bnc.bv.read(self.bnc.pos, 1).decode('charmap')) & 0b11110000)).to_bytes(1, 'big')) 97 | 98 | self.hexCursor += 0.5 99 | self.bnc.pos+= 1 100 | if self.bnc.pos% self.bnc.program.settings["hexLineLength"] == 0: 101 | self.hexOffset += 1 102 | else: 103 | self.bnc.bv.write(self.bnc.pos, (int(key, 16) << 4 | 104 | (ord(self.bnc.bv.read(self.bnc.pos, 1).decode('charmap')) & 0b00001111)).to_bytes(1, 'big')) 105 | 106 | self.hexCursor += 0.5 107 | 108 | # Adjust for off screen 109 | if self.hexOffset < 0: 110 | self.hexOffset = 0 111 | elif self.hexOffset > self.hexScreen.getmaxyx()[0]-3: 112 | self.hexOffset = self.hexScreen.getmaxyx()[0]-3 113 | 114 | if self.bnc.pos< self.bnc.bv.start: 115 | self.bnc.pos= self.bnc.bv.start 116 | self.hexCursor = self.bnc.pos 117 | elif self.bnc.pos> self.bnc.bv.end: 118 | self.bnc.pos= self.bnc.bv.end 119 | self.hexCursor = self.bnc.pos+ 0.5 120 | 121 | # if self.hexOffset == skippedLinesLine: 122 | # if key == self.bnc.program.settings["hexViewLineUp"] or key == self.bnc.program.settings["hexViewPageUp"]: 123 | # self.hexOffset -= 1 124 | # if key == self.bnc.program.settings["hexViewLineDown"] or key == self.bnc.program.settings["hexViewPageDown"]: 125 | # self.hexOffset += 1 126 | # self.bnc.pos= self.br.offset 127 | 128 | self.loadHexLines() 129 | 130 | def render(self): 131 | self.hexScreen.erase() 132 | self.hexScreen.border() 133 | 134 | for yLine, rawBytes in enumerate(self.hexLines): 135 | if yLine == self.hexOffset: 136 | for xLine, rawByte in enumerate(rawBytes): 137 | if self.hexCursor%self.bnc.program.settings["hexLineLength"] == (xLine-8-3)//3 + ((xLine-8-3)%3)/2.0 and (xLine-8-3)%3 != 2: 138 | self.hexScreen.addstr(yLine+1, 2+xLine, rawByte, curses.A_STANDOUT) 139 | else: 140 | self.hexScreen.addstr(yLine+1, 2+xLine, rawByte) 141 | else: 142 | self.hexScreen.addstr(yLine+1, 2, rawBytes) 143 | 144 | title = "Hex" 145 | drawMultiLineText(0, self.hexScreen.getmaxyx()[1]-len(title)-3, title, self.hexScreen) 146 | self.hexScreen.noutrefresh(0, 0, 0, 0, curses.LINES-2, curses.COLS-1) 147 | -------------------------------------------------------------------------------- /Logos/large.logo: -------------------------------------------------------------------------------- 1 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%. .%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%(/, .,*#%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 3 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#/,. .,*/(%%%&&@@@@@@@@@@@&&&%%#/*,. ,*&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 4 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%/, .,/#&&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&&#/,. .#&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 5 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&(/. ./#&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%(, .*&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 6 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#/. *%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%* *#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 7 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&#,. *#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#/ #&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 8 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@(, ,/&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&/, *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&,. .#&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&&@@&%%%%@@&&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%. *#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@(* ,/@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%###(((((((((((((((((((((((###%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@(, *@@@@@@@@@@@@@@@@@@@@@@@@@@@ 11 | @@@@@@@@@@@@@@@@@@@@@@@@@@@(, /%@@@@@@@@@@@@@@@@@@@@@@@@&##(((((((((((((((((((((((((((((((((((((((##&&@@@@@@@@@@@@@@@@@@@@@@@%, *&@@@@@@@@@@@@@@@@@@@@@@@@ 12 | @@@@@@@@@@@@@@@@@@@@@@@@&, *&@@@@@@@@@@@@@@@@@@@@@&%#(((((((((((((((((((((((((((((((((((((((((((((((((#%%@@@@@@@@@@@@@@@@@@@@@@*. #@@@@@@@@@@@@@@@@@@@@@@ 13 | @@@@@@@@@@@@@@@@@@@@@@&, ./@@@@@@@@@@@@@@@@@@@@&%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((#%%@@@@@@@@@@@@@@@@@@@@(. #@@@@@@@@@@@@@@@@@@@@ 14 | @@@@@@@@@@@@@@@@@@@@&, ./@@@@@@@@@@@@@@@@@@@&%#((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#%@@@@@@@@@@@@@@@@@@@(, @@@@@@@@@@@@@@@@@@@ 15 | @@@@@@@@@@@@@@@@@@@(, ./@@@@@@@@@@@@@@@@@@&##(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((##&@@@@@@@@@@@@@@@@@@(. *@@@@@@@@@@@@@@@@@ 16 | @@@@@@@@@@@@@@@@@(* *@@@@@@@@@@@@@@@@@&%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#%&@@@@@@@@@@@@@@@@@* *@@@@@@@@@@@@@@@ 17 | @@@@@@@@@@@@@@@&, ./@@@@@@@@@@@@@@@@@##(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((##&@@@@@@@@@@@@@@@@(. #@@@@@@@@@@@@@ 18 | @@@@@@@@@@@@@@(. .&@@@@@@@@@@@@@@@@##((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#&@@@@@@@@@@@@@@@@, *@@@@@@@@@@@@ 19 | @@@@@@@@@@@@&( #@@@@@@@@@@@@@@@@##((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#&@@@@@@@@@@@@@@@# *#@@@@@@@@@@ 20 | @@@@@@@@@@@@, #@@@@@@@@@@@@@@@##((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((%@@@@@@@@@@@@@@@% @@@@@@@@@@ 21 | @@@@@@@@@@#, .(@@@@@@@@@@@@@@&#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#&@@@@@@@@@@@@@@#, *@@@@@@@@ 22 | @@@@@@@@@%. .&@@@@@@@@@@@@@@%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((##@@@@@@@@@@@@@@@. #@@@@@@@ 23 | @@@@@@@@(, *%@@@@@@@@@@@@@&#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#%@@@@@@@@@@@@@&/ *@@@@@@ 24 | @@@@@@@%. .@@@@@@@@@@@@@@&#((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((%@@@@@@@@@@@@@@, #@@@@@ 25 | @@@@@@#, *%@@@@@@@@@@@@@%#((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#@@@@@@@@@@@@@&* *@@@@ 26 | @@@@@@, .&@@@@@@@@@@@@@%(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((%&@@@@@@@@@@@@&. @@@@ 27 | @@@@@/ .(@@@@@@@@@@@@@#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#@@@@@@@@@@@@@#, ,@@@ 28 | @@@@/. #@@@@@@@@@@@@@%(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((%&@@@@@@@@@@@@% ,@@ 29 | @@@@. ,@@@@@@@@@@@@@%#((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#@@@@@@@@@@@@@, @@ 30 | @@@/ .#@@@@@@@@@@@@&(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((&@@@@@@@@@@@@%, ,@ 31 | @@(, (@@@@@@@@@@@@&#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#&@@@@@@@@@@@@# * 32 | @@* &@@@@@@@@@@@@%(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#@@@@@@@@@@@@&. . 33 | @@ *@@@@@@@@@@@@&((((((((#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@((((((((%@@@@@@@@@@@@/ 34 | @. ,%@@@@@@@@@@@@%((((((((#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@((((((((#&@@@@@@@@@@@%, 35 | @, *&@@@@@@@@@@@&#((((((((#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@((((((((#%@@@@@@@@@@@&/ 36 | . #@@@@@@@@@@@@#(((((((((#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@(((((((((#@@@@@@@@@@@@% 37 | %@@@@@@@@@@@@#(((((((((#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@(((((((((#@@@@@@@@@@@@% 38 | &@@@@@@@@@@@@#(((((((((#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@((((((((((&@@@@@@@@@@@&. 39 | .&@@@@@@@@@@@@((((((((((#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@((((((((((&@@@@@@@@@@@@. 40 | .@@@@@@@@@@@@&((((((((((#%@@@@@@@@@@@&/..............................................................................,@@@@@@@@@@@@@((((((((((@@@@@@@@@@@@@, 41 | .@@@@@@@@@@@@&((((((((((#%@@@@@@@@@@@&* .@@@@@@@@@@@@@((((((((((@@@@@@@@@@@@@, 42 | .@@@@@@@@@@@@&((((((((((#%@@@@@@@@@@@&* .@@@@@@@@@@@@@((((((((((@@@@@@@@@@@@@, 43 | .&@@@@@@@@@@@@#(((((((((#%@@@@@@@@@@@&* .,**,.. .@@@@@@@@@@@@@((((((((((&@@@@@@@@@@@@. 44 | &@@@@@@@@@@@@#(((((((((#%@@@@@@@@@@@&* /%@@@@@@@@@@%*. .@@@@@@@@@@@@@((((((((((@@@@@@@@@@@@& 45 | %@@@@@@@@@@@@#(((((((((#%@@@@@@@@@@@&* (&@@@@@@@@@@@@@@@* .@@@@@@@@@@@@@(((((((((#@@@@@@@@@@@@% 46 | . (@@@@@@@@@@@@%#(((((((((#@@@@@@@@@@@@( ,@@@@@@@@@@@@@@@@@&( *@@@@@@@@@@@@%(((((((((#@@@@@@@@@@@@% 47 | @. *%@@@@@@@@@@@&#(((((((((#@@@@@@@@@@@@@* (&@@@@@@@@@@@@@@@@@@@/. (&@@@@@@@@@@@@%((((((((#%@@@@@@@@@@@&/ 48 | @ .#@@@@@@@@@@@@%((((((((((@@@@@@@@@@@@@@&/, ,*%@@@@@@@@@@@@@@@@@@@@@@@@#/. .,(&@@@@@@@@@@@@@&#((((((((#@@@@@@@@@@@@%, 49 | @@ ,@@@@@@@@@@@@@#(((((((((%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#(((((((((@@@@@@@@@@@@@, 50 | @@. %@@@@@@@@@@@@%#(((((((((#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%((#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%(((((((((#@@@@@@@@@@@@& . 51 | @@@. /&@@@@@@@@@@@@%((((((((((%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#((((%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@##((((((((#&@@@@@@@@@@@&( .@ 52 | @@@ .(@@@@@@@@@@@@@#((((((((((%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&#((((((#&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&#((((((((((&@@@@@@@@@@@@(. @ 53 | @@@@ .&@@@@@@@@@@@@%#((((((((((#%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%#((((((((##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%((((((((((#%@@@@@@@@@@@@@. @@ 54 | @@@@ /&@@@@@@@@@@@@&(((((((((((((#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#((((((((((((((#%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%##(((((((((((%@@@@@@@@@@@@@( @@ 55 | @@@@@ /@@@@@@@@@@@@@%#(((((((((((((((##%&@@@@@@@@@@@@@@@@@@@@@@&%##(((((((((((((((((((((##%%&@@@@@@@@@@@@@@@@@@@@@@&%%##((((((((((((((#@@@@@@@@@@@@@/. @@@ 56 | @@@@@@ %@@@@@@@@@@@@@&(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((%@@@@@@@@@@@@@% @@@@ 57 | @@@@@@@ .#@@@@@@@@@@@@@%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#%@@@@@@@@@@@@@#, @@@@@ 58 | @@@@@@@@ &@@@@@@@@@@@@@&#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#&@@@@@@@@@@@@@&. @@@@@@ 59 | @@@@@@@@@ ,#@@@@@@@@@@@@@&%(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#&@@@@@@@@@@@@@#, @@@@@@@ 60 | @@@@@@@@@@ %@@@@@@@@@@@@@@%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#%@@@@@@@@@@@@@@% @@@@@@@@ 61 | @@@@@@@@@@@ *@@@@@@@@@@@@@@@#((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((##@@@@@@@@@@@@@@@/. @@@@@@@@@ 62 | @@@@@@@@@@@@ /@@@@@@@@@@@@@@@%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#&@@@@@@@@@@@@@@@/ @@@@@@@@@@ 63 | @@@@@@@@@@@@@@ /%@@@@@@@@@@@@@@@%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((##@@@@@@@@@@@@@@@&/ @@@@@@@@@@@@ 64 | @@@@@@@@@@@@@@@ %@@@@@@@@@@@@@@@@%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((##@@@@@@@@@@@@@@@@% @@@@@@@@@@@@@ 65 | @@@@@@@@@@@@@@@@@ #@@@@@@@@@@@@@@@@&%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#%&@@@@@@@@@@@@@@@@# @@@@@@@@@@@@@@@ 66 | @@@@@@@@@@@@@@@@@@ .%@@@@@@@@@@@@@@@@@&##((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#&@@@@@@@@@@@@@@@@@&. @@@@@@@@@@@@@@@@ 67 | @@@@@@@@@@@@@@@@@@@@ ,&@@@@@@@@@@@@@@@@@@%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#%@@@@@@@@@@@@@@@@@@&, @@@@@@@@@@@@@@@@@@ 68 | @@@@@@@@@@@@@@@@@@@@@ ,&@@@@@@@@@@@@@@@@@@@%#(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#%&@@@@@@@@@@@@@@@@@@&, @@@@@@@@@@@@@@@@@@@ 69 | @@@@@@@@@@@@@@@@@@@@@@@ .%@@@@@@@@@@@@@@@@@@@@&##(((((((((((((((((((((((((((((((((((((((((((((((((((((((##%&@@@@@@@@@@@@@@@@@@@&, @@@@@@@@@@@@@@@@@@@@@ 70 | @@@@@@@@@@@@@@@@@@@@@@@@@ .#@@@@@@@@@@@@@@@@@@@@@@&##(((((((((((((((((((((((((((((((((((((((((((((((##%&@@@@@@@@@@@@@@@@@@@@@#. @@@@@@@@@@@@@@@@@@@@@@@ 71 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@ ./@@@@@@@@@@@@@@@@@@@@@@@@@%%##(((((((((((((((((((((((((((((((((((##%%@@@@@@@@@@@@@@@@@@@@@@@@@/ @@@@@@@@@@@@@@@@@@@@@@@@@@ 72 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .%@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%%###(((((((((((((((((((###%%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@%, @@@@@@@@@@@@@@@@@@@@@@@@@@@@ 73 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ *(@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@(* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 74 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .(%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&(. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 75 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .,#&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&#,. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 76 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ *(&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&(* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 77 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .,#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#,. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 78 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .,/(%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%(/,. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 79 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ..,*//((##%%%%%##((//*,.. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ --------------------------------------------------------------------------------