88import string
99import struct
1010import sys
11+ import textwrap
1112from typing import Any , Dict , Iterable , List , Optional , Tuple , Type , Union
1213from urllib import parse , request
1314
2324except ImportError :
2425 has_capstone = False
2526
27+ try :
28+ from IPython import terminal
29+ from traitlets import config as traitlets_config
30+
31+ has_ipython = True
32+ except ImportError :
33+ has_ipython = False
34+
2635
2736class Volshell (interfaces .plugins .PluginInterface ):
2837 """Shell environment to directly interact with a memory image."""
@@ -51,7 +60,13 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
5160 description = "File to load and execute at start" ,
5261 default = None ,
5362 optional = True ,
54- )
63+ ),
64+ requirements .BooleanRequirement (
65+ name = "script-only" ,
66+ description = "Exit volshell after the script specified in --script completes" ,
67+ default = False ,
68+ optional = True ,
69+ ),
5570 ]
5671 return reqs + [
5772 requirements .TranslationLayerRequirement (
@@ -69,43 +84,70 @@ def run(
6984 """
7085
7186 # Try to enable tab completion
72- try :
73- import readline
74- except ImportError :
75- pass
76- else :
77- import rlcompleter
87+ if not has_ipython :
88+ try :
89+ import readline
90+ import rlcompleter
7891
79- completer = rlcompleter .Completer (namespace = self ._construct_locals_dict ())
80- readline .set_completer (completer .complete )
81- readline .parse_and_bind ("tab: complete" )
82- print ("Readline imported successfully" )
92+ completer = rlcompleter .Completer (
93+ namespace = self ._construct_locals_dict ()
94+ )
95+ readline .set_completer (completer .complete )
96+ readline .parse_and_bind ("tab: complete" )
97+ print ("Readline imported successfully" )
98+ except ImportError :
99+ print (
100+ "Readline or rlcompleter module could not be imported. Tab completion will not be available."
101+ )
83102
84103 # TODO: provide help, consider generic functions (pslist?) and/or providing windows/linux functions
85104
86105 mode = self .__module__ .split ("." )[- 1 ]
87106 mode = mode [0 ].upper () + mode [1 :]
88107
89- banner = f"""
90- Call help() to see available functions
108+ banner = textwrap .dedent (
109+ f"""
110+ Call help() to see available functions
91111
92- Volshell mode : { mode }
93- Current Layer : { self .current_layer }
94- Current Symbol Table : { self .current_symbol_table }
95- Current Kernel Name : { self .current_kernel_name }
96- """
112+ Volshell mode : { mode }
113+ Current Layer : { self .current_layer }
114+ Current Symbol Table : { self .current_symbol_table }
115+ Current Kernel Name : { self .current_kernel_name }
116+ """
117+ )
97118
98119 sys .ps1 = f"({ self .current_layer } ) >>> "
99120 # Dict self._construct_locals_dict() will have priority on keys
100121 combined_locals = additional_locals .copy ()
101122 combined_locals .update (self ._construct_locals_dict ())
102- self .__console = code .InteractiveConsole (locals = combined_locals )
123+ if has_ipython :
124+
125+ class LayerNamePrompt (terminal .prompts .Prompts ):
126+ def in_prompt_tokens (self , cli = None ):
127+ slf = self .shell .user_ns .get ("self" )
128+ layer_name = slf .current_layer if slf else "no_layer"
129+ return [(terminal .prompts .Token .Prompt , f"[{ layer_name } ]> " )]
130+
131+ c = traitlets_config .Config ()
132+ c .TerminalInteractiveShell .prompts_class = LayerNamePrompt
133+ c .InteractiveShellEmbed .banner2 = banner
134+ self .__console = terminal .embed .InteractiveShellEmbed (
135+ config = c , user_ns = combined_locals
136+ )
137+ else :
138+ self .__console = code .InteractiveConsole (locals = combined_locals )
103139 # Since we have to do work to add the option only once for all different modes of volshell, we can't
104140 # rely on the default having been set
105141 if self .config .get ("script" , None ) is not None :
106142 self .run_script (location = self .config ["script" ])
107143
108- self .__console .interact (banner = banner )
144+ if self .config .get ("script-only" ):
145+ exit ()
146+
147+ if has_ipython :
148+ self .__console ()
149+ else :
150+ self .__console .interact (banner = banner )
109151
110152 return renderers .TreeGrid ([("Terminating" , str )], None )
111153
@@ -277,23 +319,25 @@ def display_bytes(self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None
277319 self ._display_data (offset , remaining_data )
278320
279321 def display_quadwords (
280- self , offset , count = DEFAULT_NUM_DISPLAY_BYTES , layer_name = None
322+ self , offset , count = DEFAULT_NUM_DISPLAY_BYTES , layer_name = None , byteorder = "@"
281323 ):
282324 """Displays quad-word values (8 bytes) and corresponding ASCII characters"""
283325 remaining_data = self ._read_data (offset , count = count , layer_name = layer_name )
284- self ._display_data (offset , remaining_data , format_string = " Q" )
326+ self ._display_data (offset , remaining_data , format_string = f" { byteorder } Q" )
285327
286328 def display_doublewords (
287- self , offset , count = DEFAULT_NUM_DISPLAY_BYTES , layer_name = None
329+ self , offset , count = DEFAULT_NUM_DISPLAY_BYTES , layer_name = None , byteorder = "@"
288330 ):
289331 """Displays double-word values (4 bytes) and corresponding ASCII characters"""
290332 remaining_data = self ._read_data (offset , count = count , layer_name = layer_name )
291- self ._display_data (offset , remaining_data , format_string = " I" )
333+ self ._display_data (offset , remaining_data , format_string = f" { byteorder } I" )
292334
293- def display_words (self , offset , count = DEFAULT_NUM_DISPLAY_BYTES , layer_name = None ):
335+ def display_words (
336+ self , offset , count = DEFAULT_NUM_DISPLAY_BYTES , layer_name = None , byteorder = "@"
337+ ):
294338 """Displays word values (2 bytes) and corresponding ASCII characters"""
295339 remaining_data = self ._read_data (offset , count = count , layer_name = layer_name )
296- self ._display_data (offset , remaining_data , format_string = " H" )
340+ self ._display_data (offset , remaining_data , format_string = f" { byteorder } H" )
297341
298342 def regex_scan (self , pattern , count = DEFAULT_NUM_DISPLAY_BYTES , layer_name = None ):
299343 """Scans for regex pattern in layer using RegExScanner."""
@@ -508,10 +552,13 @@ def run_script(self, location: str):
508552 location = "file:" + request .pathname2url (location )
509553 print (f"Running code from { location } \n " )
510554 accessor = resources .ResourceAccessor ()
511- with accessor .open (url = location ) as fp :
512- self .__console .runsource (
513- io .TextIOWrapper (fp , encoding = "utf-8" ).read (), symbol = "exec"
514- )
555+ with accessor .open (url = location ) as handle , io .TextIOWrapper (
556+ handle , encoding = "utf-8"
557+ ) as fp :
558+ if has_ipython :
559+ self .__console .ex (fp .read ())
560+ else :
561+ self .__console .runsource (fp .read (), symbol = "exec" )
515562 print ("\n Code complete" )
516563
517564 def load_file (self , location : str ):
0 commit comments