├── .gitignore ├── README.md ├── changelog.md ├── docs ├── bookmarks.png ├── decode.png └── img.png ├── src ├── stackstack.py └── stackstack │ ├── __init__.py │ ├── scan.py │ ├── sue.py │ ├── trace.py │ └── utils.py └── version /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | /.idea/ 132 | .DS_Store 133 | .python-version 134 | .pytest_cache/ 135 | backup_files/ 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StackStack 2 | 3 | Simple Unicorn emulation plugin. I originally developed the plugin as a quick way to emulate decoding strings obfuscated with 4 | [ADVObfuscator](https://github.com/andrivet/ADVobfuscator) or similar methods. 5 | 6 | ## Installation 7 | 8 | - Copy src/stackstack.py and src/stackstack/ to your Ida Plugins directory. 9 | - Restart Ida 10 | 11 | ## Requirements 12 | - Unicorn Emulator 13 | - Yara 14 | - Keystone engine 15 | - Capstone engine 16 | 17 | ## Configuration 18 | 19 | - `loglevel`: Log level to use (DEBUG, ERROR, INFO...). Default: `DEBUG` 20 | - `ext_yara_file`: External yara file to use for automated scanning. Defaults to `stackem.yara` 21 | - `bookmarks`: Create bookmarks at decoded offsets. Default: `True` 22 | - `rename_func`: Rename function which contains a single AdvObfuscated string. This is useful where a function 23 | encapsulates a call to a native API. Default: `False` 24 | - `check_update`: Check if there is an update available. 25 | 26 | Example config file 27 | ``` 28 | { 29 | 'loglevel': 'DEBUG', \ 30 | 'ext_yara_file': 'stackstack.yara', 31 | 'bookmarks': True, 32 | 'rename_func': False, 33 | 'check_update': True 34 | } 35 | ``` 36 | 37 | 38 | ## Modes 39 | 40 | ### Decode 41 | 42 | Emulates the current block or selected bytes and attempts to extract the decoded bytes. 43 | 44 | - Decode Selected - Emulate the selected bytes 45 | - Decode Current - Based on the current cursor position in the decompiler window. Detect the blocks to emulate. 46 | - Shortcut: `shift-x` 47 | - Decode All - Scan for and attempt to decode each identified block. 48 | - Decode Function - Scan the current function and attempt to decode the found blocks. 49 | 50 | ![Example of decode options](docs/decode.png "Decode Options") 51 | 52 | ### Bookmarks 53 | To help navigate the binary, the plugin can create a bookmark at the location of each decoded string. If this is configured, bookmarks will be created with the prefix `SSB:` 54 | 55 | ![Example of created bookmarks](docs/bookmarks.png "Bookmarks") 56 | 57 | ### Trace 58 | 59 | For now add register values as a comment and at the end of the block emit the last val 60 | for each register. 61 | 62 | ### Emulate 63 | 64 | Emulate the current block and return the end state of all registers. 65 | 66 | ### Scan 67 | 68 | Scan for ADVObfuscated Strings or matches based on the passed yara rules. 69 | 70 | Shortcut `shift-s` 71 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.07 - 2022-01-15 4 | - Updates to better handle different obfuscation methods. 5 | - Refactoring 6 | - Update how plugin detects hexrays 7 | - Remove POC patching code 8 | - Minor updates and bug fixes 9 | 10 | ## 1.06 - 2021-08-25 11 | - BUGFIX: Fix issue with the emulator stack setup 12 | - Improve backtracing 32bit code blocks 13 | - Minor updates 14 | 15 | ## 1.05 - 2021-07-27 16 | - BUGFIX: Fix issue with detecting encoded blobs could cause Ida to become unresponsive 17 | - BUGFIX: Fix issues with decode current function. 18 | - BUGFIX: Fix issue with orphaned comments in decompile window. 19 | - BUGFIX: Fix issue with routines that operate directly on memory offsets and do not trigger a memory read. 20 | - BUGFIX: Fix issue with unicode strings of unknown length. 21 | - Improve scanning for encrypted strings in 32bit binaries 22 | - Improve emulation 23 | - Improve auto-detect of blocks 24 | - Add base to use code blocks to quickly detect code to emulate 25 | - General code cleanup and optimizations 26 | - Check for update disabled by default 27 | 28 | ## 1.04 - 2021-05-24 29 | - BUGFIX: Fix issue with no comment being added 30 | - Update: Hide nop range by default 31 | - Minor updates 32 | 33 | ## 1.03 - 2021-05-19 34 | 35 | - BUGFIX: Issue #2 - Changed the default behaviour to add a comment to the Hexrays view. 36 | - Added check for missing config properties and add them to the config. 37 | - Minor updates. 38 | 39 | ## 1.02 - 2021-05-17 40 | 41 | - Code cleanup 42 | - Update internal scan rules to include 32 & 64 bit rules. 43 | - BUGFIX: Issues with emulating 32bit code and reading from RIP when emulating x86 code 44 | - BUGFIX: FIX issue when the start of function is used as the blob start. First check if there are any calls or other data that was exected to be initialized. 45 | - BUGFIX: FIX issue where patch bytes are null. 46 | - KNOWN ISSUE: 32bit patching is disabled. Patching is being refactored and improved. 47 | 48 | ## 1.0 - 2021-05-05 49 | - Initial Release 50 | -------------------------------------------------------------------------------- /docs/bookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idiom/stackstack/4567acaf21812a48c32b11f4da3cb0329a73d526/docs/bookmarks.png -------------------------------------------------------------------------------- /docs/decode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idiom/stackstack/4567acaf21812a48c32b11f4da3cb0329a73d526/docs/decode.png -------------------------------------------------------------------------------- /docs/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idiom/stackstack/4567acaf21812a48c32b11f4da3cb0329a73d526/docs/img.png -------------------------------------------------------------------------------- /src/stackstack.py: -------------------------------------------------------------------------------- 1 | import ida_idaapi 2 | import ida_kernwin 3 | import idaapi 4 | import ida_ua 5 | import ida_diskio 6 | import idautils 7 | 8 | import idc 9 | import os 10 | import json 11 | import logging 12 | 13 | from stackstack.scan import YaraScanner 14 | from stackstack.sue import SUE 15 | from stackstack.utils import IdaHelpers, Update 16 | 17 | BAD = [0xffffffff, 0xffffffffffffffff] 18 | 19 | 20 | class StackStack(object): 21 | 22 | def __init__(self, logger): 23 | 24 | self.logger = logger 25 | 26 | self.last_bookmark = 0 27 | self.arch = IdaHelpers.get_arch() 28 | 29 | def find_end(self, offset): 30 | function_end = idc.get_func_attr(offset, idc.FUNCATTR_END) 31 | self.logger.debug('Function End: %x' % function_end) 32 | 33 | found_compare = False 34 | 35 | while offset <= function_end: 36 | ins = ida_ua.insn_t() 37 | idaapi.decode_insn(ins, offset) 38 | 39 | self.logger.debug('%x: %s' % (offset, idc.generate_disasm_line(offset, 0))) 40 | 41 | if ins.itype == idaapi.NN_jmp: 42 | return idc.next_head(offset, function_end) 43 | elif ins.itype == idaapi.NN_call: 44 | return idc.next_head(offset, function_end) 45 | elif ins.itype in [idaapi.NN_jmp, idaapi.NN_jnz, idaapi.NN_jb]: 46 | if found_compare: 47 | return idc.next_head(offset, function_end) 48 | elif ins.itype == idaapi.NN_cmp: 49 | if idc.get_operand_type(offset, 0) == idc.o_reg \ 50 | and idc.get_operand_type(offset, 1) == idc.o_imm: 51 | try: 52 | self.logger.debug("Found compare..") 53 | found_compare = True 54 | string_length = int(idc.print_operand(offset, 1)) + 1 55 | self.logger.debug("Setting String length: %d" % string_length) 56 | except ValueError: 57 | string_length = int(idc.print_operand(offset, 1)[:-1], 16) + 1 58 | self.logger.debug("Setting String length: %d" % string_length) 59 | elif ins.itype == idaapi.NN_dec: 60 | if idc.get_operand_type(offset, 0) == idc.o_reg: 61 | found_compare = True 62 | offset = idc.next_head(offset, function_end) 63 | return 0 64 | 65 | def get_string_length(self, offset): 66 | function_end = idc.get_func_attr(offset, idc.FUNCATTR_END) 67 | self.logger.debug('Function End: %x' % function_end) 68 | 69 | while offset <= function_end: 70 | ins = ida_ua.insn_t() 71 | idaapi.decode_insn(ins, offset) 72 | 73 | if ins.itype == idaapi.NN_jmp: 74 | return 0 75 | elif ins.itype == idaapi.NN_call: 76 | return 0 77 | elif ins.itype == idaapi.NN_cmp: 78 | if idc.get_operand_type(offset, 0) == idc.o_reg \ 79 | and idc.get_operand_type(offset, 1) == idc.o_imm: 80 | try: 81 | self.logger.debug("Found compare..") 82 | return int(idc.print_operand(offset, 1)) + 1 83 | except ValueError: 84 | return int(idc.print_operand(offset, 1)[:-1], 16) + 1 85 | offset = idc.next_head(offset, function_end) 86 | 87 | return 0 88 | 89 | def detect_blocks(self, offset, trace_end=False): 90 | """ 91 | Use ida basic blocks to detect last block etc. 92 | 93 | :param offset: 94 | :param trace_end: 95 | :return: 96 | """ 97 | pass 98 | 99 | def _has_call(self, start, end): 100 | 101 | while start < end: 102 | ins = ida_ua.insn_t() 103 | idaapi.decode_insn(ins, start) 104 | if ins.itype == idaapi.NN_call: 105 | self.logger.debug("Found call at 0x%x..skipping using function start." % start) 106 | return True 107 | start = idc.next_head(start, end) 108 | return False 109 | 110 | def scan_reg_increment(self, blob_start, offset): 111 | """ 112 | 113 | Check if the block uses a register to increment the counter. Some samples will have something similar to 114 | 115 | 116 | xor r15d, r15d 117 | 118 | lea r12d, [r15+1] 119 | 120 | add rax, r12 121 | 122 | :return: None if not found or identified register 123 | """ 124 | 125 | function_start = idc.get_func_attr(offset, idc.FUNCATTR_START) 126 | cursor = 0 127 | 128 | found_compare = False 129 | found_operand = False 130 | cmp_reg = None 131 | inc_reg = None 132 | while offset >= blob_start: 133 | 134 | if offset <= function_start: 135 | return None 136 | 137 | cursor += 1 138 | ins = ida_ua.insn_t() 139 | idaapi.decode_insn(ins, offset) 140 | 141 | self.logger.debug('%x: %s' % (offset, idc.generate_disasm_line(offset, 0))) 142 | 143 | if not found_compare: 144 | if ins.itype == idaapi.NN_cmp: 145 | if idc.get_operand_type(offset, 0) == idc.o_reg: 146 | cmp_reg = idc.get_operand_value(offset, 0) 147 | found_compare = True 148 | 149 | if found_compare: 150 | if ins.itype == idaapi.NN_add: 151 | if idc.get_operand_type(offset, 0) == idc.o_reg \ 152 | and idc.get_operand_value(offset, 0) == cmp_reg \ 153 | and idc.get_operand_type(offset, 1) == idc.o_reg: 154 | #inc_reg = idc.get_operand_value(offset, 1) 155 | inc_reg = idc.print_operand(offset, 1) 156 | return inc_reg 157 | #cmp_reg = idc.get_operand_value(offset, 0) 158 | 159 | elif ins.itype == idaapi.NN_add: 160 | if found_operand: 161 | if idc.get_operand_type(offset, 0) == idc.o_reg and idc.get_operand_value(offset, 0) == cmp_reg: 162 | # first op is a reg and matches the expected 163 | if idc.get_operand_type(offset, 1) == idc.o_reg: 164 | inc_reg = idc.print_operand(offset, 1) 165 | self.logger.debug("Found inc reg :: %s" % inc_reg) 166 | return inc_reg 167 | # second op is a reg 168 | 169 | offset = idc.prev_head(offset, function_start) 170 | return None 171 | 172 | def backtrace_start(self, offset, max_instructions=1024): 173 | """ 174 | From the current offset - backtrace and find the best instruction to start emulation. 175 | 176 | :param offset: 177 | :param max_instructions: The maximum number of instructions to backtrace 178 | :return: 179 | """ 180 | function_start = idc.get_func_attr(offset, idc.FUNCATTR_START) 181 | blob_start = 0 182 | last_mov_or_alt = True 183 | 184 | trace_instruction_types = [idaapi.NN_mov, 185 | idaapi.NN_sub, 186 | idaapi.NN_xor, 187 | idaapi.NN_lea, 188 | idaapi.NN_add, 189 | idaapi.NN_inc, 190 | idaapi.NN_movupd, 191 | idaapi.NN_movups, 192 | idaapi.NN_movaps, 193 | idaapi.NN_movapd,] 194 | 195 | # Back trace 196 | if offset <= function_start + 64: 197 | """ 198 | Note this can cause issues. If there is a call or other that is expected to have 199 | initialized data. Do a quick check first before returning the function start 200 | """ 201 | 202 | if not self._has_call(function_start, offset): 203 | return function_start 204 | icount = 0 205 | while offset >= function_start: 206 | icount += 1 207 | 208 | if icount > max_instructions: 209 | return 0 210 | 211 | ins = ida_ua.insn_t() 212 | 213 | idaapi.decode_insn(ins, offset) 214 | self.logger.debug("0x%x %s" % (offset, idc.generate_disasm_line(idc.prev_head(offset), 0))) 215 | 216 | if ins.itype in trace_instruction_types: 217 | if ins.itype in [idaapi.NN_mov]: 218 | last_mov_or_alt = True 219 | elif ins.itype == idaapi.NN_xor: 220 | if idc.print_operand(offset, 0) != idc.print_operand(offset, 1): 221 | if idc.get_operand_type(offset, 1) != idaapi.o_imm: 222 | blob_start = idc.next_head(offset) 223 | break 224 | else: 225 | last_mov_or_alt = True 226 | else: 227 | last_mov_or_alt = False 228 | elif ins.itype == idaapi.NN_sub: 229 | if not last_mov_or_alt: 230 | blob_start = idc.next_head(offset) 231 | break 232 | last_mov_or_alt = False 233 | elif ins.itype in [idaapi.NN_lea, idaapi.NN_inc, idaapi.NN_add]: 234 | last_mov_or_alt = True 235 | 236 | blob_start = offset 237 | 238 | if offset <= function_start: 239 | self.logger.debug("Error back-tracing ADVBLOB...Using function start") 240 | blob_start = function_start 241 | break 242 | 243 | else: 244 | blob_start = idc.next_head(offset) 245 | break 246 | 247 | offset = idc.prev_head(offset) 248 | 249 | if blob_start <= function_start + 64: 250 | if not self._has_call(function_start, blob_start): 251 | blob_start = function_start 252 | 253 | self.logger.debug("BLOB Start: %x" % blob_start) 254 | self.logger.debug(idc.print_insn_mnem(blob_start)) 255 | return blob_start 256 | 257 | 258 | class DecodeHandler(ida_kernwin.action_handler_t): 259 | """ 260 | Handle hot key and mouse actions. 261 | 262 | """ 263 | 264 | def __init__(self, logger, set_bookmarks=True, has_hexrays=False): 265 | ida_kernwin.action_handler_t.__init__(self) 266 | self.scanner = YaraScanner(logger) 267 | self.logger = logger 268 | self.has_hexrays = has_hexrays 269 | 270 | self.set_bookmarks = set_bookmarks 271 | self.mode = int(IdaHelpers.get_arch()/8) 272 | self.stacks = StackStack(logger) 273 | 274 | def trace_bytes(self): 275 | start = idc.read_selection_start() 276 | end = idc.read_selection_end() 277 | 278 | if start in BAD: 279 | self.logger.error("Nothing selected") 280 | idc.warning("No instructions selected!") 281 | return 282 | 283 | self.logger.debug("Selection Start: 0x%x" % start) 284 | self.logger.debug("Selection End: 0x%x" % end) 285 | 286 | if not start or not end: 287 | idc.warning("Error: Range not selected") 288 | return 289 | 290 | def _identify_impl_type(self, start, end): 291 | """ 292 | Identify the implementation type. 293 | 294 | 0 - func - string is decrypted in a call to a function. Offset is returned in eax 295 | 1 - inline1 - 296 | 297 | TODO: This should be expanded to better detect different implementations 298 | 299 | :param start: 300 | :param end: 301 | :return: 302 | """ 303 | 304 | self.logger.debug('End Param: %x' % end) 305 | ins = ida_ua.insn_t() 306 | idaapi.decode_insn(ins, idc.prev_head(end)) 307 | idc.generate_disasm_line(idc.prev_head(end), 0) 308 | self.logger.debug("0x%x %s" % (end, idc.generate_disasm_line(idc.prev_head(end), 0))) 309 | if ins.itype == idaapi.NN_call: 310 | self.logger.debug("Ends in a call") 311 | return 0 312 | 313 | def _process(self, start, end, string_length=0): 314 | self.logger.debug("_process->enter") 315 | 316 | if start >= end: 317 | self.logger.error("End block before start!") 318 | self.logger.debug("Start: %x" % start) 319 | self.logger.debug("End: %x" % end) 320 | return 321 | 322 | if end: 323 | ireg = self.stacks.scan_reg_increment(start, end) 324 | iregs = [] 325 | if ireg: 326 | self.logger.debug("[*] Initializing [%s] to 1" % ireg) 327 | iregs.append((ireg, 1)) 328 | 329 | self.logger.debug("[*] Using ImageBase: %x" % idaapi.get_imagebase()) 330 | 331 | impl_type = self._identify_impl_type(start, end) 332 | 333 | semu = SUE(code_base=idaapi.get_imagebase(), mode=self.mode, logger=self.logger) 334 | 335 | sresult = semu.deobfuscate_stack(start, end, string_length=string_length, impl_type=impl_type, init_regs=iregs) 336 | 337 | self.logger.debug("...Complete....") 338 | 339 | if not sresult: 340 | self.logger.debug("No result data!") 341 | return 342 | 343 | self.logger.debug("-" * 16) 344 | for rk in sresult.keys(): 345 | try: 346 | self.logger.debug("%s: 0x%x" % (rk, sresult[rk])) 347 | except TypeError: 348 | self.logger.debug("%s: %s" % (rk, sresult[rk])) 349 | self.logger.debug("-" * 16) 350 | 351 | decoded = sresult['data'] 352 | 353 | if decoded: 354 | if self.set_bookmarks: 355 | IdaHelpers.add_bookmark(start, decoded) 356 | IdaHelpers.add_comment(start, decoded, hexrays=self.has_hexrays) 357 | return decoded 358 | 359 | def process_matches(self, matches, function_start): 360 | """ 361 | 362 | :param matches: 363 | :return: 364 | """ 365 | 366 | if not matches: 367 | return 368 | 369 | # stacks = StackStack(self.logger) 370 | 371 | last_start = 0 372 | last_end = 0 373 | decoded_strings = [] 374 | deocded_offsets = [] 375 | for match in matches: 376 | try: 377 | match_offset = function_start + match 378 | self.logger.debug("Processing match offset: %x" % match_offset) 379 | block_start = self.stacks.backtrace_start(match_offset) 380 | if not block_start: 381 | self.logger.error("Could not find block start for %x .. skipping" % match_offset) 382 | continue 383 | 384 | block_end = self.stacks.find_end(block_start) 385 | if not block_end: 386 | self.logger.error("Could not find block end for %x .. skipping" % match_offset) 387 | continue 388 | 389 | if last_end == 0 == last_start: 390 | last_end = block_end 391 | last_start = block_start 392 | else: 393 | if last_start < match_offset < last_end: 394 | self.logger.debug("Skipping offset [%x]" % match_offset) 395 | continue 396 | 397 | 398 | string_length = self.stacks.get_string_length(block_start) 399 | self.logger.debug("Processing: start: %x, end: %x, string_length: %d" % (block_start, block_end, string_length)) 400 | decoded = self._process(block_start, block_end, string_length=string_length) 401 | if decoded: 402 | if not block_start in deocded_offsets: 403 | decoded_strings.append((block_start, decoded)) 404 | deocded_offsets.append(block_start) 405 | 406 | self.logger.debug("Using start of: %x" % block_start) 407 | except Exception as ex: 408 | self.logger.error("Error processing block: %s" % ex) 409 | 410 | return decoded_strings 411 | 412 | def update(self, ctx): 413 | """ 414 | Required by action handler 415 | """ 416 | return ida_kernwin.AST_ENABLE_ALWAYS 417 | 418 | def activate(self, ctx): 419 | if ctx.action == 'ssp_decode_selected': 420 | self.decode_selected() 421 | elif ctx.action == 'ssp_decode_current': 422 | self.decode_current() 423 | elif ctx.action == 'ssp_decode_func': 424 | self.decode_function() 425 | elif ctx.action == 'ssp_trace_selected': 426 | self.trace_bytes() 427 | elif ctx.action == 'ssp_decode_all': 428 | self.decode_all() 429 | else: 430 | self.logger.debug(ctx.cur_func) 431 | self.logger.debug("Not Supported") 432 | idc.warning("Not Implemented") 433 | return True 434 | 435 | def decode_function(self): 436 | 437 | # advs = StackStack(self.logger) 438 | offset = idaapi.get_item_head(idc.here()) 439 | 440 | function_name = idc.get_func_name(offset) 441 | 442 | self.logger.debug("Processing: %s" % function_name) 443 | 444 | function_start = idc.get_func_attr(offset, idc.FUNCATTR_START) 445 | function_end = idc.get_func_attr(offset, idc.FUNCATTR_END) 446 | 447 | fdata = idc.get_bytes(function_start, function_end - function_start) 448 | 449 | refs = self.scanner.scan_function(fdata) 450 | 451 | if refs: 452 | self.logger.debug("Found [%d] Obfuscated Strings" % len(refs)) 453 | 454 | decoded_strings = self.process_matches(refs, function_start) 455 | if decoded_strings: 456 | # TODO: Add this to a UI Pop-up 457 | print("--- Decoded Function Strings ---") 458 | for o, s in decoded_strings: 459 | print(" %x: %s" % (o, s)) 460 | 461 | else: 462 | self.logger.debug("No code blocks found..") 463 | 464 | def decode_all(self): 465 | try: 466 | # ss = StackStack(self.logger) 467 | decoded_strings = [] 468 | for func_entry in idautils.Functions(): 469 | if idc.get_func_attr(func_entry, idc.FUNCATTR_FLAGS) & idc.FUNC_LIB: 470 | self.logger.debug("Skipping lib function %s" % idc.get_func_name(func_entry)) 471 | continue 472 | 473 | fdata = idc.get_bytes(func_entry, idc.get_func_attr(func_entry, idc.FUNCATTR_END) - func_entry) 474 | refs = self.scanner.scan_function(fdata) 475 | if refs: 476 | self.logger.debug("Found [%d] Obfuscated Strings" % len(refs)) 477 | decoded_strings.extend(self.process_matches(refs, func_entry)) 478 | 479 | if decoded_strings: 480 | # TODO: Add this to a UI Pop-up 481 | print("--- Decoded Function Strings ---") 482 | for o, s in decoded_strings: 483 | print(" %x: %s" % (o, s)) 484 | except Exception as ex: 485 | self.logger.error("Error processing file: %s" % ex) 486 | 487 | def decode_current(self): 488 | # stacks = StackStack(self.logger) 489 | 490 | offset = idaapi.get_item_head(idc.here()) 491 | self.logger.debug("Starting scan at offset: %x" % offset) 492 | 493 | # TODO: Make this a configuration option to either use back trace or blocks 494 | start = self.stacks.backtrace_start(offset) 495 | 496 | self.logger.debug("Snippet Start: %x" % start) 497 | 498 | if start: 499 | end = self.stacks.find_end(start) 500 | if end: 501 | string_length = self.stacks.get_string_length(start) 502 | self.logger.debug("Processing: start: %x, end: %x, string_length: %d" % (start, end, string_length)) 503 | self._process(start, end, string_length=string_length) 504 | else: 505 | self.logger.error("Did not find end. Manually select instructions!") 506 | 507 | else: 508 | idc.warning("Decode Failed! Unable to get ADVBlock") 509 | 510 | def decode_selected(self): 511 | 512 | start = idc.read_selection_start() 513 | end = idc.read_selection_end() 514 | 515 | if start in BAD: 516 | self.logger.debug("Nothing selected") 517 | idc.warning("Nothing Selected!") 518 | return 519 | 520 | self.logger.debug("Selection Start: 0x%x" % start) 521 | self.logger.debug("Selection End: 0x%x" % end) 522 | 523 | if start: 524 | self._process(start, end) 525 | else: 526 | idc.warning("Selected Block ") 527 | 528 | 529 | class ScanHandler(ida_kernwin.action_handler_t): 530 | 531 | def __init__(self, logger): 532 | self.logger = logger 533 | ida_kernwin.action_handler_t.__init__(self) 534 | self.scanner = YaraScanner(logger) 535 | 536 | def update(self, ctx): 537 | return ida_kernwin.AST_ENABLE_ALWAYS 538 | 539 | def _scan_bin(self): 540 | self.logger.info("Scanning file...") 541 | scan_result = self.scanner.scan_functions() 542 | self.logger.info("Scan complete...") 543 | if not scan_result: 544 | self.logger.error("Found no suspect code blocks!") 545 | return 546 | 547 | self.logger.info("-" * 16) 548 | self.logger.info("Found %d suspect functions" % len(scan_result.keys())) 549 | for ref in scan_result: 550 | self.logger.info('%s\t%d' % (ref, scan_result[ref])) 551 | self.logger.info("-" * 16) 552 | 553 | def activate(self, ctx): 554 | if ctx.action == 'ssp_scan': 555 | self._scan_bin() 556 | else: 557 | self.logger.error("Unsupported scan option :: %s" % ctx.action) 558 | idc.warning("Unsupported Scan Option") 559 | return True 560 | 561 | def term(self): 562 | pass 563 | 564 | 565 | class StackStackConfig(object): 566 | pass 567 | 568 | 569 | class StackStackPlugin(ida_idaapi.plugin_t): 570 | 571 | flags = ida_idaapi.PLUGIN_KEEP 572 | 573 | comment = "StackStack simple emulation and tracing" 574 | help = "StackStack - simple emulation and tracing" 575 | wanted_name = "StackStack" 576 | wanted_hotkey = "" 577 | 578 | _version = 1.07 579 | 580 | def init(self): 581 | try: 582 | self.config = self.load_configuration() 583 | try: 584 | self.logger = self._init_logger(logging._checkLevel(self.config['loglevel'].upper())) 585 | except ValueError: 586 | self.logger = self._init_logger(self.logger.setLevel(logging.DEBUG)) 587 | 588 | # TODO: Remove 589 | self.logger.setLevel(logging.DEBUG) 590 | 591 | self.logger.info("StackStack version: %s" % StackStackPlugin._version) 592 | 593 | if self.config['check_update']: 594 | version_check = Update.check_version(StackStackPlugin._version) 595 | if version_check > 0: 596 | idc.warning("StackStack version %s is now available for download." % version_check) 597 | 598 | self.has_hexrays = IdaHelpers.has_decompiler() 599 | 600 | self.actions = [] 601 | self.define_actions() 602 | self.menus = Menus() 603 | self.menus.hook() 604 | except Exception as ex: 605 | self.logger.error('Error initializing StackStack %s' % ex) 606 | idc.warning('Error initializing StackStack %s' % ex) 607 | 608 | return ida_idaapi.PLUGIN_KEEP 609 | 610 | def _init_logger(self, loglevel, name='stackstack'): 611 | """ 612 | Initialize Logger 613 | 614 | :param loglevel: Log Level to use 615 | :param name: Log name 616 | :return: 617 | """ 618 | # logging.basicConfig() 619 | # logging.root.setLevel(logging.NOTSET) 620 | 621 | logger = logging.getLogger(name) 622 | # logger.setLevel(loglevel) 623 | logger.setLevel(loglevel) 624 | 625 | log_stream = logging.StreamHandler() 626 | formatter = logging.Formatter('stackstack:%(levelname)s:%(message)s') 627 | log_stream.setFormatter(formatter) 628 | logger.addHandler(log_stream) 629 | return logger 630 | 631 | def load_configuration(self, config_name='stackstack.cfg', generate_default_config=True): 632 | path = ida_diskio.get_user_idadir() 633 | config_path = os.path.join(path, config_name) 634 | config_data = None 635 | 636 | if os.path.exists(config_path): 637 | with open(config_path, 'r') as inf: 638 | config_data = json.loads(inf.read()) 639 | 640 | if config_data: 641 | def_config = self._generate_default_configuration() 642 | missing_options = False 643 | for key in def_config.keys(): 644 | # iterate through the default config keys and add any missing config entries. 645 | try: 646 | config_data[key] 647 | except KeyError: 648 | config_data[key] = def_config[key] 649 | missing_options = True 650 | if missing_options: 651 | with open(config_path, 'w') as out: 652 | out.write(json.dumps(config_data)) 653 | return config_data 654 | 655 | if generate_default_config: 656 | config_data = self._generate_default_configuration() 657 | with open(config_path, 'w') as out: 658 | out.write(json.dumps(config_data)) 659 | return config_data 660 | 661 | def _generate_default_configuration(self): 662 | return { 663 | 'loglevel': 'DEBUG', 664 | 'ext_yara_file': 'stackstack.yara', 665 | 'bookmarks': True, 666 | 'rename_func': False, 667 | 'check_update': False 668 | } 669 | 670 | def _get_scan_actions(self): 671 | scanner = ScanHandler(self.logger) 672 | return [ 673 | ida_kernwin.action_desc_t( 674 | "ssp_scan", 675 | "Scan", 676 | scanner, 677 | "Shift-s", 678 | "Scan binary for functions with encrypted strings" 679 | )] 680 | 681 | def _get_decode_actions(self): 682 | decode = DecodeHandler(self.logger, has_hexrays=self.has_hexrays) 683 | return [ 684 | ida_kernwin.action_desc_t( 685 | "ssp_trace_selected", 686 | "Trace Selected", 687 | decode, 688 | "Trace the selected bytes." 689 | ), 690 | ida_kernwin.action_desc_t( 691 | "ssp_decode_selected", 692 | "Decode Selected", 693 | decode, 694 | "Decode the selected bytes." 695 | ), 696 | ida_kernwin.action_desc_t( 697 | "ssp_decode_current", 698 | "Decode Current", 699 | decode, 700 | "Shift-x", 701 | "Detect and decode the current obfuscated bytes." 702 | ), 703 | ida_kernwin.action_desc_t( 704 | "ssp_decode_all", 705 | "Decode All", 706 | decode, 707 | "Scan and decode all instances." 708 | ), 709 | ida_kernwin.action_desc_t( 710 | "ssp_decode_func", 711 | "Decode Function", 712 | decode, 713 | "Decode all instances in the current function." 714 | ) 715 | ] 716 | 717 | def _get_util_actions(self): 718 | return [ 719 | ida_kernwin.action_desc_t( 720 | "ssp_decode_func", 721 | "Decode Function", 722 | DecodeHandler(has_hexrays=self.has_hexrays), 723 | "Decode all instances in the current function." 724 | ) 725 | ] 726 | 727 | def define_actions(self): 728 | actions = [] 729 | actions.extend(self._get_scan_actions()) 730 | actions.extend(self._get_decode_actions()) 731 | self.actions = actions 732 | for action_desc in actions: 733 | ida_kernwin.register_action(action_desc) 734 | 735 | def run(self, arg): 736 | print('-------------------------------------') 737 | print('running\n\n\n\n\n\n\n\n\n') 738 | print('-------------------------------------') 739 | pass 740 | 741 | def term(self): 742 | if self.actions: 743 | for action_desc in self.actions: 744 | ida_kernwin.unregister_action(action_desc.name) 745 | 746 | 747 | class Menus(ida_kernwin.UI_Hooks): 748 | 749 | def finish_populating_widget_popup(self, form, popup): 750 | 751 | if ida_kernwin.get_widget_type(form) in [ida_kernwin.BWN_PSEUDOCODE]: 752 | ida_kernwin.attach_action_to_popup(form, popup, "ssp_decode_current", "StackStack/Decode/") 753 | 754 | if ida_kernwin.get_widget_type(form) == ida_kernwin.BWN_DISASM: 755 | ida_kernwin.attach_action_to_popup(form, popup, "ssp_decode_selected", "StackStack/Decode/") 756 | ida_kernwin.attach_action_to_popup(form, popup, "ssp_trace_selected", "StackStack/Trace/") 757 | 758 | if ida_kernwin.get_widget_type(form) in [ida_kernwin.BWN_PSEUDOCODE, ida_kernwin.BWN_DISASM]: 759 | ida_kernwin.attach_action_to_popup(form, popup, "ssp_scan", "StackStack/") 760 | ida_kernwin.attach_action_to_popup(form, popup, "ssp_decode_all", "StackStack/Decode/") 761 | ida_kernwin.attach_action_to_popup(form, popup, "ssp_decode_func", "StackStack/Decode/") 762 | 763 | def PLUGIN_ENTRY(): 764 | return StackStackPlugin() 765 | -------------------------------------------------------------------------------- /src/stackstack/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idiom/stackstack/4567acaf21812a48c32b11f4da3cb0329a73d526/src/stackstack/__init__.py -------------------------------------------------------------------------------- /src/stackstack/scan.py: -------------------------------------------------------------------------------- 1 | import yara 2 | import idautils 3 | import idc 4 | import idaapi 5 | import logging 6 | import os.path 7 | 8 | 9 | class ScanEngineBase(object): 10 | pass 11 | 12 | 13 | class YaraScanner(ScanEngineBase): 14 | """ 15 | 16 | """ 17 | 18 | def __init__(self, logger, rule_file=None, rules=[]): 19 | self.logger = logger 20 | self.logger.info("Init YaraScanner") 21 | 22 | x64_rules = [ 23 | """rule scan_a{strings: $ = {c6 45 ?? 00 c6 45 ?? ?? c6 45} condition: all of them}""", 24 | """rule scan_b{strings: $ = {c6 85 [2-3] ff ff 00 [0-2] c6 85 [2-3] ff ff} condition: all of them}""", 25 | """rule scan_c{strings: $ = {(c6|c7) 4? [2-6] (c6|c7) 4? } condition: all of them}""", 26 | """rule scan_d{strings: $ = {(c6|c7) 4? ?? 00 (c6|c7) 4? ?? ?? (c6|c7) 4? } condition: all of them}""", 27 | """rule scan_e{strings: $ = {(c6|c7) 85 [4] ?? 00 00 00 [0-5] (c6|c7) 85 ?? 0? 00 00 ??} condition: all of them}""", 28 | """rule scan_f{strings: $ = {(c6|c7) 4? [2-3] 00 00 00 [0-5] 8B 4? ??} condition: all of them}""", 29 | """rule scan_g{strings: $ = {(c6|c7) 85 ?? 0? 00 00 ?? (c6|c7) 85 ?? 0? 00 00 ??} condition: all of them}""", 30 | ] 31 | 32 | x86_rules = [ 33 | """rule scan_a{strings: $ = {c6 4? [2-3] c6 4? [2-3] c6 4? [2-3] c6 4?} condition: all of them}""", 34 | """rule scan_b{strings: $ = {c6 4? [2-3] c6 4? [2-3] c6 4? [2-3] c6 4?} condition: all of them}""", 35 | """rule scan_c{strings: $ = {8b ?? ?? ff ff ff 34 ?? 88 ?? ?? ff ff ff 8b ?? ?? ff ff ff} condition: all of them}""", 36 | """rule scan_d{strings: $ = {c7 00 [4] c7 40 [5] c7 40} condition: all of them}""", 37 | """rule scan_e{strings: $ = {c6 85 [5] c6 85 [5] c6 85} condition: all of them}""", 38 | """rule scan_f{strings: $ = {0f 28 05 [4] 0f 11 [5] 0f} condition: all of them}""" 39 | ] 40 | 41 | self.raw_rules = [] 42 | 43 | if idaapi.get_inf_structure().is_64bit(): 44 | self.raw_rules = x64_rules 45 | else: 46 | self.raw_rules = x86_rules 47 | 48 | self.raw_rules.extend(rules) 49 | self.rules = self._compile_rules(self.raw_rules) 50 | 51 | if rule_file: 52 | self.logger.info(f"Processing rule file {rule_file}") 53 | self._compile_ext_rules(rule_file) 54 | 55 | def _compile_ext_rules(self, ext_rule_path): 56 | if not os.path.isfile(ext_rule_path): 57 | return 58 | try: 59 | compiled = yara.compile(filepath=ext_rule_path) 60 | if not self.rules: 61 | self.rules = [] 62 | self.rules.append(compiled) 63 | except Exception as ex: 64 | self.logger.error("Error loading rulefile: %s" % ex) 65 | 66 | def _compile_rules(self, rules): 67 | compiled = [] 68 | self.logger.error("Compiling rules") 69 | for rule in rules: 70 | try: 71 | compiled.append(yara.compile(source=rule)) 72 | except Exception as ex: 73 | self.logger.debug(" [!] Error compiling rule: %s" % ex) 74 | return compiled 75 | 76 | def scan_functions(self, ignore_libs=True, match_overlay_range=64): 77 | """ 78 | 79 | Scan all functions optionally excluding lib functions 80 | 81 | :param ignore_libs: Ignore library functions 82 | :param match_overlay_range: Exclude results within this range of the last match 83 | :return: 84 | """ 85 | results = {} 86 | 87 | self.logger.error("Scanning functions") 88 | for func_entry in idautils.Functions(): 89 | if ignore_libs: 90 | if idc.get_func_attr(func_entry, idc.FUNCATTR_FLAGS) & idc.FUNC_LIB: 91 | self.logger.debug("Skipping lib function %s" % idc.get_func_name(func_entry)) 92 | continue 93 | 94 | func_name = idc.get_func_name(func_entry) 95 | 96 | func_data = idc.get_bytes(func_entry, idc.get_func_attr(func_entry, idc.FUNCATTR_END) - func_entry) 97 | match_strings = [] 98 | for rule in self.rules: 99 | self.logger.debug("Skipping lib function %s" % idc.get_func_name(func_entry)) 100 | matches = rule.match(data=func_data) 101 | 102 | if not matches: 103 | continue 104 | 105 | for rule_match in matches: 106 | for match in rule_match.strings: 107 | match_strings.append(match) 108 | 109 | last_match_offset = 0 110 | for match in sorted(match_strings, key=lambda d: d[0]): # order matches by ascending offset 111 | if last_match_offset > match[0] > last_match_offset - match_overlay_range: 112 | continue 113 | elif last_match_offset < match[0] < last_match_offset + match_overlay_range: 114 | continue 115 | 116 | self.logger.debug("Match at [%x]" % (func_entry + match[0])) 117 | last_match_offset = match[0] 118 | try: 119 | results[func_name] += 1 120 | except KeyError: 121 | results[func_name] = 1 122 | return results 123 | 124 | def scan_function(self, data, match_overlay_range=64): 125 | """ 126 | Scan the current function for obfuscated blobs 127 | 128 | :param data: 129 | :param match_overlay_range: 130 | :return: 131 | """ 132 | values = [] 133 | match_strings = [] 134 | self.logger.error("Scanning function") 135 | for rule in self.rules: 136 | matches = rule.match(data=data) 137 | 138 | if not matches: 139 | continue 140 | 141 | for rule_match in matches: 142 | for match in rule_match.strings: 143 | match_strings.append(match) 144 | 145 | # Assign last_match_offset with the negative value of match_overlay_range 146 | # to avoid skipping matches at the beginning of the function 147 | last_match_offset = -abs(match_overlay_range) 148 | for match in sorted(match_strings, key=lambda d: d[0]): # order matches by ascending offset 149 | if last_match_offset > match[0] > last_match_offset - match_overlay_range: 150 | continue 151 | elif last_match_offset < match[0] < last_match_offset + match_overlay_range: 152 | continue 153 | 154 | self.logger.debug("Match at %x" % match[0]) 155 | last_match_offset = match[0] 156 | values.append(match[0]) 157 | return values 158 | 159 | -------------------------------------------------------------------------------- /src/stackstack/sue.py: -------------------------------------------------------------------------------- 1 | from unicorn import * 2 | from unicorn.x86_const import * 3 | from enum import Enum 4 | 5 | import idaapi 6 | import idautils 7 | import idc 8 | import ida_ua 9 | import logging 10 | 11 | 12 | class EmulationTimeout(Exception): 13 | pass 14 | 15 | 16 | class IdaUnicornMap(Enum): 17 | pass 18 | 19 | 20 | class BaseEmulator(object): 21 | 22 | RegisterMap = { 23 | "ecx": UC_X86_REG_ECX, 24 | "edx": UC_X86_REG_EDX, 25 | "ebx": UC_X86_REG_EBX, 26 | "eax": UC_X86_REG_EAX, 27 | "esi": UC_X86_REG_ESI, 28 | "edi": UC_X86_REG_EDI, 29 | "ebp": UC_X86_REG_EBP, 30 | "esp": UC_X86_REG_ESP, 31 | "eip": UC_X86_REG_EIP, 32 | "r8d": UC_X86_REG_R8D, 33 | "r9d": UC_X86_REG_R9D, 34 | "r10d": UC_X86_REG_R10D, 35 | "r11d": UC_X86_REG_R11D, 36 | "r12d": UC_X86_REG_R12D, 37 | "r13d": UC_X86_REG_R13D, 38 | "r14d": UC_X86_REG_R14D, 39 | "r15d": UC_X86_REG_R15D, 40 | "r8w": UC_X86_REG_R8W, 41 | "r9w": UC_X86_REG_R9W, 42 | "r10w": UC_X86_REG_R10W, 43 | "r11w": UC_X86_REG_R11W, 44 | "r12w": UC_X86_REG_R12W, 45 | "r13w": UC_X86_REG_R13W, 46 | "r14w": UC_X86_REG_R14W, 47 | "r15w": UC_X86_REG_R15W, 48 | "r8b": UC_X86_REG_R8B, 49 | "r9b": UC_X86_REG_R9B, 50 | "r10b": UC_X86_REG_R10B, 51 | "r11b": UC_X86_REG_R11B, 52 | "r12b": UC_X86_REG_R12B, 53 | "r13b": UC_X86_REG_R13B, 54 | "r14b": UC_X86_REG_R14B, 55 | "r15b": UC_X86_REG_R15B, 56 | "rip": UC_X86_REG_RIP, 57 | "rax": UC_X86_REG_RAX, 58 | "rbx": UC_X86_REG_RBX, 59 | "rcx": UC_X86_REG_RCX, 60 | "rdx": UC_X86_REG_RDX, 61 | "rsi": UC_X86_REG_RSI, 62 | "rdi": UC_X86_REG_RDI, 63 | "rbp": UC_X86_REG_RBP, 64 | "rsp": UC_X86_REG_RSP, 65 | "r8": UC_X86_REG_R8, 66 | "r9": UC_X86_REG_R9, 67 | "r10": UC_X86_REG_R10, 68 | "r11": UC_X86_REG_R11, 69 | "r12": UC_X86_REG_R12, 70 | "r13": UC_X86_REG_R13, 71 | "r14": UC_X86_REG_R14, 72 | "r15": UC_X86_REG_R15, 73 | "xmm0": UC_X86_REG_XMM0, 74 | "xmm1": UC_X86_REG_XMM1, 75 | "xmm2": UC_X86_REG_XMM2, 76 | "xmm3": UC_X86_REG_XMM3 77 | } 78 | 79 | MemoryAccessLookup = { 80 | UC_MEM_READ: "UC_MEM_READ", 81 | UC_MEM_FETCH: "UC_MEM_FETCH", 82 | UC_MEM_READ_UNMAPPED: "UC_MEM_READ_UNMAPPED", 83 | UC_MEM_WRITE_UNMAPPED: "UC_MEM_WRITE_UNMAPPED", 84 | UC_MEM_FETCH_UNMAPPED: "UC_MEM_FETCH_UNMAPPED", 85 | UC_MEM_WRITE_PROT: "UC_MEM_WRITE_PROT", 86 | UC_MEM_FETCH_PROT: "UC_MEM_FETCH_PROT", 87 | UC_MEM_READ_AFTER: "UC_MEM_READ_AFTER" 88 | } 89 | 90 | def __init__(self, 91 | code_base=0x18000000, 92 | stack_base=0xA0000000, 93 | stack_size=0x10000, 94 | mode=UC_MODE_64, 95 | logger=None): 96 | """ 97 | 98 | :param code_base: Base address to use for code 99 | :param stack_base: Base address to use for stack 100 | :param stack_size: Size of stack in bytes 101 | :param mode: UC_MODE to use (UC_MODE_32/UC_MODE_64) 102 | :param loglevel: The loglevel to use with the logger 103 | :param handle_mem_read_errors: Attempt to skip mem read errors 104 | :param trace: Enable Instruction tracing 105 | """ 106 | 107 | self.code_base = code_base 108 | self.stack_base = stack_base 109 | self.stack_size = stack_size 110 | self.mode = mode 111 | 112 | self.logger = logger 113 | 114 | if not logger: 115 | self.logger = logging.getLogger('stackstack') 116 | self.logger.setLevel(logging.DEBUG) 117 | 118 | def map_full_file(self, mu): 119 | for seg in idautils.Segments(): 120 | data = None 121 | cur_seg = idaapi.getseg(seg) 122 | size = cur_seg.end_ea - cur_seg.start_ea 123 | if size > 0: 124 | data = idc.get_bytes(cur_seg.start_ea, size) 125 | if data is None: 126 | continue 127 | mu.mem_write(cur_seg.start_ea, data) 128 | 129 | def setup_emulator(self): 130 | mu = None 131 | mu = Uc(UC_ARCH_X86, self.mode) 132 | 133 | end_address = self._get_end_address() 134 | 135 | # align 136 | size = end_address - self.code_base + (0x1000 - end_address % 0x1000) 137 | mu.mem_map(self.code_base, size) 138 | 139 | return mu 140 | 141 | def _get_end_address(self): 142 | end_address = 0 143 | 144 | for segment in idautils.Segments(): 145 | if idaapi.getseg(segment).end_ea > end_address: 146 | end_address = idaapi.getseg(segment).end_ea 147 | return end_address 148 | 149 | def emulate(self, start_address, end_address, hooks, timeout=1, init_regs=[]): 150 | """ 151 | 152 | :param start_address: 153 | :param end_address: 154 | :param hooks: 155 | :param timeout: 156 | :param init_regs: Array of tuples used to initialize register values before emulation 157 | :return: 158 | """ 159 | mu = self.setup_emulator() 160 | self.map_full_file(mu) 161 | 162 | for reg in SUE.RegisterMap.values(): 163 | mu.reg_write(reg, 0) 164 | 165 | if init_regs: 166 | for idef in init_regs: 167 | mu.reg_write(SUE.RegisterMap[idef[0]], idef[1]) 168 | 169 | # Setup stack 170 | mu.mem_map(self.stack_base, self.stack_size) 171 | stack_offset = int(self.stack_base + (self.stack_size / 2)) 172 | mu.reg_write(UC_X86_REG_ESP, stack_offset) 173 | mu.reg_write(UC_X86_REG_EBP, stack_offset) 174 | 175 | # Add hooks 176 | if hooks: 177 | for hook in hooks: 178 | mu.hook_add(hook[0], hook[1]) 179 | 180 | self.logger.debug("Starting Emulation") 181 | 182 | # emulate code 183 | mu.emu_start(start_address, end_address, timeout=timeout * UC_SECOND_SCALE) 184 | 185 | if timeout > 0: 186 | ip = UC_X86_REG_EIP 187 | if self.mode == UC_MODE_64: 188 | ip = UC_X86_REG_RIP 189 | ip_offset = mu.reg_read(ip) 190 | self.logger.debug("RIP: %x" % ip_offset) 191 | self.logger.debug("Expected end: %x" % end_address) 192 | if end_address != ip_offset: 193 | raise EmulationTimeout() 194 | 195 | self.logger.debug("Emulation Complete..") 196 | 197 | return mu 198 | 199 | 200 | class SUE(BaseEmulator): 201 | """ 202 | An emulator named SUE (Simple Unicorn Emulator) 203 | 204 | """ 205 | 206 | def __init__(self, 207 | code_base=0x18000000, 208 | stack_base=0xA0000000, 209 | stack_size=0x10000, 210 | mode=UC_MODE_64, 211 | loglevel=logging.DEBUG, 212 | handle_mem_read_errors=True, 213 | trace=True, 214 | logger=None): 215 | 216 | """ 217 | 218 | 219 | :param code_base: Base address to use for code 220 | :param stack_base: Base address to use for stack 221 | :param stack_size: Size of stack in bytes 222 | :param mode: UC_MODE to use (UC_MODE_32/UC_MODE_64) 223 | :param loglevel: The loglevel to use with the logger 224 | :param handle_mem_read_errors: Attempt to skip mem read errors 225 | :param trace: Enable Instruction tracing 226 | """ 227 | 228 | self.stack_data = "" 229 | self.debug = debug 230 | self.trace = trace 231 | self.read_switch = False 232 | self.decoded_stack = "" 233 | self.mode = mode 234 | 235 | self.write_switch = False 236 | self.last_write_address = 0 237 | self.first_write_address = 0 238 | self.handle_mem_read_errors = handle_mem_read_errors 239 | 240 | self.write_address_list = [] 241 | 242 | super().__init__(code_base, stack_base, stack_size, mode, logger) 243 | 244 | def hook_mem_access(self, uc, access, address, size, value, user_data): 245 | """ 246 | 247 | """ 248 | if access == UC_MEM_WRITE: 249 | if self.trace: 250 | self.logger.debug("Memory Write at 0x%x, data size = %u, data value = 0x%x" % (address, size, value)) 251 | # TODO: clean this up 252 | self.write_switch = True 253 | try: 254 | if address not in self.write_address_list: 255 | self.write_address_list.append(address) 256 | else: 257 | if not self.first_write_address: 258 | self.first_write_address = address 259 | self.write_address_list.append(address) 260 | 261 | if size == 1: 262 | if self.read_switch: 263 | if not self.first_write_address: 264 | self.first_write_address = address 265 | self.decoded_stack += chr(value) 266 | self.last_write_address = address 267 | 268 | except Exception as ex: 269 | self.logger.error("Memory Hook exception: %s" % ex) 270 | 271 | else: 272 | if size == 1: 273 | if self.first_write_address == 0: 274 | if self.write_switch: 275 | self.first_write_address = address 276 | if self.first_write_address > 0: 277 | self.read_switch = True 278 | 279 | if self.trace: 280 | self.logger.debug("Memory READ at 0x%x, data size = %u" % (address, size)) 281 | 282 | def hook_code(self, mu, address, size, user_data): 283 | if self.trace: 284 | self.logger.info('TRACE: 0x%x, instruction size = 0x%x' % (address, size)) 285 | 286 | def hook_patch_inc(self, mu, address, size, user_data): 287 | if self.trace: 288 | self.logger.info('TRACE: 0x%x, instruction size = 0x%x' % (address, size)) 289 | # rip = mu.reg_read(UC_X86_REG_RIP) 290 | # self._debug_log("TRACE: RIP is 0x%x" % rip) 291 | 292 | iobj = ida_ua.insn_t() 293 | idaapi.decode_insn(iobj, address) 294 | 295 | if iobj.itype == idaapi.NN_add: 296 | self.logger.debug('Found Add Instruction') 297 | if iobj.Op1.type == idaapi.o_reg: 298 | self.logger.debug('Op1 is reg') 299 | if iobj.Op2.type == idaapi.o_reg: 300 | self.logger.debug('Op2 is reg') 301 | reg = idc.print_operand(address, 1) 302 | self.logger.debug('Register: %s' % reg) 303 | val = mu.reg_read(self.RegisterMap[reg]) 304 | self.logger.debug('Register Value: %x' % val) 305 | if val == 0: 306 | self.logger.debug('Setting %s to 1' % reg) 307 | mu.reg_write(self.RegisterMap[reg], 1) 308 | 309 | def hook_mem_invalid(self, uc, access, address, size, value, user_data): 310 | rip = uc.reg_read(UC_X86_REG_RIP) 311 | 312 | if access == UC_MEM_WRITE: 313 | self.logger.debug( 314 | "Invalid WRITE of 0x%x at 0x%X, data size = %u, data value = 0x%x" % (address, rip, size, value)) 315 | else: 316 | self.logger.debug( 317 | "Invalid %s of 0x%x at 0x%X, data size = %u" % (SUE.MemoryAccessLookup[access], address, rip, size)) 318 | 319 | if self.handle_mem_read_errors: 320 | self.logger.debug("Read error..attempting to handle") 321 | 322 | if address == 0: 323 | # Trying to read from null 324 | # for now return False 325 | return False 326 | if access == UC_MEM_READ_UNMAPPED: 327 | # Attempt to allocate memory at the specified address 328 | mem_size = size + (1024 - size % 1024) 329 | uc.mem_map(address, mem_size) 330 | return True 331 | return False 332 | 333 | def _get_printable_stack(self, mu): 334 | try: 335 | sdata = mu.mem_read(self.stack_base, self.stack_size) 336 | sdata = sdata.strip(b"\x00") 337 | return sdata.decode('utf-8', 'ignore') 338 | except TypeError as te: 339 | self.logger.error(te) 340 | return "" 341 | 342 | def _get_func_decoded(self, mu, mode): 343 | result = {} 344 | data = '' 345 | reg = 'rax' 346 | if mode == UC_MODE_32: 347 | reg = 'eax' 348 | rax_value = mu.reg_read(self.RegisterMap[reg]) 349 | 350 | if rax_value > 0: 351 | length = 8 352 | while True: 353 | data = mu.mem_read(rax_value, length) 354 | if data[-2:] == b"\x00\x00": 355 | break 356 | length += 8 357 | if length > 512: 358 | break 359 | 360 | if data[1] == 0: 361 | data = data.decode("utf-16").strip("\x00") 362 | else: 363 | data = data.decode("utf-8").strip("\x00") 364 | 365 | if data: 366 | result['data'] = data 367 | result['data_length'] = len(data) 368 | return result 369 | 370 | def _decode_data(self, indata): 371 | 372 | if indata[1] == 0: 373 | try: 374 | return indata.decode("utf-16") 375 | except UnicodeDecodeError: 376 | # probably have bad data 377 | # just swallow this for now 378 | # Which is probably a bad idea 379 | self.logger.debug("Error decoding as unicode") 380 | pass 381 | else: 382 | return indata.decode("utf-8") 383 | 384 | def deobfuscate_stack(self, start_address, end_address, retry=0, string_length=0, impl_type=-1, init_regs=[]): 385 | """ 386 | Primarily tested with ADVObfuscated strings. Works with similar methods which write obfuscated bytes to the 387 | stack, deobfucstate them, and return the result. 388 | 389 | :param start_address: 390 | :param end_address: 391 | :param retry: 392 | :param string_length: 393 | :param impl_type: 394 | :return: 395 | """ 396 | 397 | self.logger.debug("[*] Initializing") 398 | result = {} 399 | 400 | try: 401 | 402 | hooks = [ 403 | (UC_HOOK_MEM_WRITE, self.hook_mem_access), 404 | (UC_HOOK_MEM_READ, self.hook_mem_access), 405 | (UC_HOOK_MEM_INVALID, self.hook_mem_invalid), 406 | (UC_HOOK_CODE, self.hook_code) 407 | ] 408 | try: 409 | mu = self.emulate(start_address, end_address, hooks, init_regs=init_regs) 410 | except EmulationTimeout: 411 | self.logger.debug("Emulation Timeout...setting patch_inc hook") 412 | hooks = [ 413 | (UC_HOOK_MEM_WRITE, self.hook_mem_access), 414 | (UC_HOOK_MEM_READ, self.hook_mem_access), 415 | (UC_HOOK_MEM_INVALID, self.hook_mem_invalid), 416 | (UC_HOOK_CODE, self.hook_patch_inc) 417 | ] 418 | mu = self.emulate(start_address, end_address, hooks) 419 | 420 | if not mu: 421 | return result 422 | 423 | for reg in self.RegisterMap.keys(): 424 | try: 425 | result[reg] = mu.reg_read(self.RegisterMap[reg]) 426 | except Exception as ex: 427 | self.logger.error("Error reading register :: %s" % reg) 428 | self.logger.error("Error: %s" % ex) 429 | 430 | stack_data = b"" 431 | 432 | # self.logger.debug("Raw: %s" % self._get_raw_stack_data(mu)) 433 | 434 | if not self.decoded_stack: 435 | self.decoded_stack = self._get_printable_stack(mu) 436 | 437 | self.logger.debug("Lazy Stack: %s\n\n" % self.decoded_stack) 438 | 439 | self.logger.debug("impl_type: %s" % impl_type) 440 | if impl_type == 0: 441 | self.logger.debug("String length: %d" % string_length) 442 | self.logger.debug("String offset: %x" % mu.reg_read(self.RegisterMap['eax'])) 443 | if string_length > 0: 444 | stack_data = mu.mem_read(mu.reg_read(self.RegisterMap['eax']), string_length) 445 | else: 446 | stack_chars = [] 447 | offset = mu.reg_read(self.RegisterMap['eax']) 448 | while offset < self.stack_base + self.stack_size: 449 | c = mu.mem_read(offset, 2) 450 | if c == b'\x00\x00': 451 | break 452 | stack_chars.append(c) 453 | offset += 2 454 | stack_data = b''.join(c for c in stack_chars) 455 | 456 | if stack_data: 457 | stack_data = self._decode_data(stack_data) 458 | result['data'] = stack_data 459 | result['data_length'] = len(stack_data) 460 | return result 461 | 462 | if self.decoded_stack: 463 | ds = self.decoded_stack.replace("\x00", "") 464 | result['data'] = ds 465 | result['data_length'] = len(ds) 466 | return result 467 | 468 | self.logger.debug("Attempting to auto extract data...") 469 | 470 | # Attempt to extract the written string from the stack. 471 | # 472 | self.stack_offset = self.stack_base + 0x1000 473 | 474 | cursor = self.first_write_address 475 | 476 | if not self.read_switch: 477 | string_length = 0 478 | 479 | self.logger.debug('Cursor: %x' % cursor) 480 | self.logger.debug('Stack Offset: %x' % self.stack_offset) 481 | self.logger.debug('Last Write Address:: %x' % self.last_write_address) 482 | self.logger.debug('String Length: %s' % string_length) 483 | 484 | if string_length > 0: 485 | self.logger.debug("Reading [%d] bytes from memory" % string_length) 486 | test = mu.mem_read(cursor, string_length) 487 | self.logger.debug('Extracted Bytes: %s' % test) 488 | result['data'] = test.decode("utf-8").replace("\x00", "") 489 | else: 490 | counter = 0 491 | last_char_null = False 492 | while cursor < self.stack_base + self.stack_size: 493 | if counter > 1000: 494 | break 495 | # counter += 1 496 | data = mu.mem_read(cursor, 1) 497 | cursor += 1 498 | 499 | if data == b"\x00": 500 | if last_char_null: 501 | break 502 | last_char_null = True 503 | counter += 1 504 | continue 505 | last_char_null = False 506 | stack_data = stack_data + data 507 | counter += 1 508 | 509 | self.logger.debug("Extracted: %s" % stack_data) 510 | self.logger.debug(type(stack_data)) 511 | 512 | if len(stack_data) > 1: 513 | if stack_data[1] == 0: 514 | try: 515 | stack_data = stack_data.decode("utf-16") 516 | except UnicodeDecodeError: 517 | # probably have bad data 518 | # just swallow this for now 519 | # Which is probably a bad idea 520 | self.logger.debug("Error decoding as unicode") 521 | pass 522 | else: 523 | stack_data = stack_data.decode("utf-8") 524 | 525 | result['data'] = stack_data 526 | result['data_length'] = len(stack_data) 527 | else: 528 | 529 | result['data'] = '' 530 | result['data_length'] = 0 531 | 532 | try: 533 | self.logger.debug('Extracted Data: %s' % result['data']) 534 | result['data_length'] = len(result['data']) 535 | except KeyError as ke: 536 | self.logger.error('Error processing data: %s' % ke) 537 | 538 | return result 539 | 540 | except Exception as e: 541 | if retry == 0: 542 | self.logger.error(" [!] Emulation error: %s" % e) 543 | self.logger.error(" [*] Retrying...") 544 | self.read_switch = False 545 | 546 | # Enable Tracing 547 | # self.trace = True 548 | self.stack_data = "" 549 | self.decoded_stack = "" 550 | return self.deobfuscate_stack(start_address, end_address, retry=1) 551 | else: 552 | self.logger.error(" [!] Fatal error emulating [%s]" % e) 553 | -------------------------------------------------------------------------------- /src/stackstack/trace.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idiom/stackstack/4567acaf21812a48c32b11f4da3cb0329a73d526/src/stackstack/trace.py -------------------------------------------------------------------------------- /src/stackstack/utils.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | import idc 3 | import idautils 4 | import http.client 5 | import ida_hexrays 6 | 7 | class IdaHelpers(object): 8 | 9 | SegmentAlignMap = { 10 | idaapi.saAbs: 0, 11 | idaapi.saRelByte: 1, 12 | idaapi.saRelWord: 2, 13 | idaapi.saRelPara: 16, 14 | idaapi.saRelPage: 256, 15 | idaapi.saRelDble: 4, 16 | idaapi.saRel32Bytes: 32, 17 | idaapi.saRel64Bytes: 64, 18 | idaapi.saRelQword: 8, 19 | idaapi.saRel512Bytes: 512, 20 | idaapi.saRel1024Bytes: 1024, 21 | idaapi.saRel2048Bytes: 2048, 22 | } 23 | 24 | @classmethod 25 | def has_decompiler(cls): 26 | """ 27 | Check if the current instance has the decompiler. 28 | 29 | :return: True/False if the decompiler is available. 30 | """ 31 | try: 32 | if not ida_hexrays.get_hexrays_version(): 33 | return False 34 | return True 35 | except Exception: 36 | return False 37 | 38 | @staticmethod 39 | def add_comment(offset, comment, hexrays=True, overwrite=True): 40 | """ 41 | Add a comment to the disassembly at the specified offset and optionally in the Hexray's decompilation. 42 | 43 | :param offset: Offset to add the comment at 44 | :param comment: The comment 45 | :param hexrays: Apply to hexrays decompile window 46 | :param overwrite: Overwrite existing comment 47 | """ 48 | if not overwrite: 49 | existing_comment = idc.GetCommentEx(offset, 0) 50 | if existing_comment: 51 | comment = "%s; %s" % (existing_comment, comment) 52 | 53 | # Add comment to disassembly 54 | idc.set_cmt(offset, 'Decoded: %s' % comment, 0) 55 | 56 | if hexrays: 57 | try: 58 | cfunc = ida_hexrays.decompile(offset) 59 | fmap = cfunc.get_eamap() 60 | tl = idaapi.treeloc_t() 61 | tl.ea = fmap[offset][0].ea 62 | tl.itp = idaapi.ITP_SEMI 63 | cfunc.set_user_cmt(tl, comment) 64 | cfunc.save_user_cmts() 65 | cfunc.refresh_func_ctext() 66 | except KeyError: 67 | pass 68 | except ida_hexrays.DecompilationFailure: 69 | pass 70 | 71 | @staticmethod 72 | def add_bookmark(offset, comment, check_duplicate=True): 73 | """ 74 | Add a bookmark and optionally skip if one exists for the offset. 75 | 76 | :param offset: 77 | :param comment: 78 | :param check_duplicate: 79 | :return: 80 | """ 81 | for bslot in range(0, 1024, 1): 82 | slotval = idc.get_bookmark(bslot) 83 | if check_duplicate: 84 | if slotval == offset: 85 | break 86 | 87 | if slotval == 0xffffffffffffffff: 88 | idc.put_bookmark(offset, 0, 0, 0, bslot, "SSB: %s" % comment) 89 | break 90 | 91 | @staticmethod 92 | def get_bitness(): 93 | if idaapi.get_inf_structure().is_64bit(): 94 | return 2 95 | return 1 96 | 97 | @staticmethod 98 | def get_arch(): 99 | if IdaHelpers.get_bitness() > 1: 100 | return 64 101 | return 32 102 | 103 | @staticmethod 104 | def add_section(offset, name, bitness, size=0x1000, base=0, cls='DATA'): 105 | """ 106 | Add a segment at the specified offset 107 | 108 | :param offset: 109 | :param name: 110 | :param bitness: 111 | :param size: 112 | :param base: 113 | :param cls: 114 | :return: 115 | """ 116 | if offset == 0: 117 | offset = idaapi.inf_get_max_ea() 118 | if offset == idaapi.BADADDR: 119 | offset = 0 120 | for s in idautils.Segments(): 121 | if offset < idc.get_segm_end(s): 122 | offset = idc.get_segm_end(s) 123 | 124 | sdef = idaapi.segment_t() 125 | 126 | flags = idaapi.ADDSEG_OR_DIE 127 | 128 | sdef.start_ea = offset 129 | sdef.end_ea = offset + size 130 | sdef.sel = idaapi.setup_selector(base) 131 | sdef.align = idaapi.saRelPara 132 | sdef.perm = idaapi.SEGPERM_READ 133 | sdef.bitness = bitness 134 | sdef.comb = idaapi.scPub 135 | 136 | idaapi.add_segm_ex(sdef, name, cls, flags) 137 | 138 | 139 | class Update(object): 140 | 141 | @staticmethod 142 | def check_version(version): 143 | """ 144 | Check if a new version of the plugin is available. 145 | 146 | :param version: 147 | :return: 148 | """ 149 | try: 150 | req = http.client.HTTPSConnection("raw.githubusercontent.com") 151 | req.request("GET", "/idiom/stackstack/main/version") 152 | res = req.getresponse() 153 | if res.status == 200: 154 | remote_ver = res.read() 155 | if float(remote_ver.decode()) > version: 156 | return float(remote_ver.decode()) 157 | return 0 158 | except Exception as ex: 159 | print("Error checking version: %s" % ex) 160 | return 0 161 | 162 | 163 | class UIWrapper(object): 164 | 165 | def __init__(self): 166 | pass -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 1.07 --------------------------------------------------------------------------------