├── README.md ├── hansel.py └── rules └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # Hansel 2 | A simple but flexible search for IDA. 3 | 4 | Bytes, strings, symbols and values from enumerated types are sometimes all that is needed to identify a function’s functionality. For example, cross-referencing `CreateToolhelp32Snapshot` and `TerminateProcess` to a single function can be used to inter that an executable might have functionality to kill a remote process. Or another example might be identifying a function as SHA256 because of the presence of `0x6a09e667` and `0xbb67ae85`. Overtime functionality can be quickly identified during the reverse engineering process because of the reappearing attributes. Hansel can be used to search for these attributes, label and comment functions with these attributes or automatically extract these attributes from a function. All of this starts with the search function. It can contain arguments of strings (`foo`), integers (`0xFFFFFFFF`) or byte patterns (`{55 8B EC}`). There are keywords that are covered later on. The goal of the search is to be as simple as possible. Nothing needs to be defined from a search perspective. The following snippet searches for `CreateToolhelp32Snapshot` and `TerminateProcess`. The return of `search` is a tuple. The first value is a bool stating if the search was `True` or `False`. The second value is a set of offsets that contains the start offset of function or functions the search matches on. 5 | 6 | ```python 7 | Python>search("CreateToolhelp32Snapshot", 0xFFFFFFFF, "TerminateProcess", ) 8 | (True, set([4249808L])) 9 | ``` 10 | To rename the function that matches the search add `”rename=NewFunctionName”`. To add a function comment that matches the search add `comment="My Comment"`. The following snippet is the previous search with function comment and labeling added. 11 | ```python 12 | search("CreateToolhelp32Snapshot", 0xFFFFFFFF, "TerminateProcess", rename="kill_process", comment="kill process" ) 13 | ``` 14 | 15 | Searches can be saved to a rules file by calling `save_search()`. This function must contain `filename="FILE.rule”`. The following snippet is the working example saved to a file named `kill_process.rule`. 16 | ```python 17 | save_search("CreateToolhelp32Snapshot", 0xFFFFFFFF, "TerminateProcess", filename="kill_process.rule", rename="kill_process", comment="kill process") 18 | ``` 19 | To search using rules saved in a file the function `run_rule(rule_name)` is used. 20 | 21 | ```python 22 | Python>run_rule("kill_process.rule") 23 | RULE(s): C:/Users/this/Documents/repo/hansel\rules\kill_process.rule 24 | SEARCH: ['CreateToolhelp32Snapshot', 4294967295L, 'TerminateProcess'] 25 | Match at 0x40e8a0 26 | ``` 27 | To run all rules the function `run_rules()` can be used. Attributes can be extracted from a function by calling `generate_skeleton(ea)`. 28 | 29 | ```python 30 | Python>generate_skeleton(here()) 31 | ['CreateToolhelp32Snapshot', 0, 2, 1, 556, 'lstrlenW', 'Process32NextW', 'OpenProcess', 'CharUpperBuffW', 'Process32FirstW', 1600, 'lstrcpyW', 4294965704L, 4294965696L, 'CloseHandle', 'TerminateProcess', 'lstrcmpW', 4294965732L, 4294966252L, 4294966772L, 4294967292L, 4294967295L] 32 | ``` 33 | To quickly save attributes from a function and revisit them at a later date, the hotkey `ALT-/` can be used. The rules are saved in the rules directory within the working directory of the Hansel repo. The rule file name is the current date `YEAR-MONTH-DAY.rule` (example: ` 2019-03-03.rule`). The search contains a field name `context` that has the IDB path and function offset. The following is an example of a rule file. 34 | ``` 35 | {"search_terms": ["CreateToolhelp32Snapshot", 0, 2, 1, 556, "lstrlenW", "Process32NextW", "OpenProcess", "CharUpperBuffW", "Process32FirstW", 1600, "lstrcpyW", 4294965704, 4294965696, "CloseHandle", "TerminateProcess", "lstrcmpW", 4294965732, 4294966252, 4294966772, 4294967292, 4294967295], "kwargs": {"context": "C:\\Users\\REMOVED\\Desktop\\foo.idb, 0x40e8a0"}} 36 | ``` 37 | 38 | The function `cheat_sheet()` can used to retrieve all the needed APIS and their keywords. 39 | 40 | ```python 41 | Python>cheat_sheet() 42 | 43 | search("query1", "query2", comment="My_Comment", rename="FUNCTION_NAME") 44 | save_search( "query1", file_name="RULE_NAME.rule", comment="My_Comment", rename="FUNCTION_NAME") 45 | run_rule("RULES_NAME.rule") 46 | run_rules() <- no arguments 47 | hot_key() <- saves output of generate_skelton(ea) to rules directory with the date as the name 48 | added by hot_key() "context=XYZ.idb" 49 | ``` 50 | 51 | Hansel uses Yara to search for strings and bytes. When loaded it copies data from the IDB into memory that Yara can search. On VMs with limited memory, the copying can take a second to load. A status is displayed on IDA’s command line. 52 | ``` 53 | Status: Loading memory for Yara. 54 | Status: Memory has been loaded. 55 | ``` 56 | If a rule throws an error it is likely because the search breaks Yara’s search syntax (mostly strings that need escape sequences). As previously mentioned Hansel returns the start of the function that contains the match. If the search is a single byte pattern rule and with no function cross-reference(s) than the byte pattern match is returned. 57 | ```python 58 | Python>search("{7F 7F 7F 7F 7F 7F 7F 7F 40 56 41 00 84 62 41 00 }") 59 | (True, [4281968L]) 60 | ``` 61 | 62 | ## Status 63 | - Stablish. 64 | - I’m still testing all the possible combinations of searches and keywords. 65 | - Daily usage so bugs will be fixed. 66 | 67 | ## Version Changes 68 | 69 | ### 2.0 70 | - removed storing the `rename`, `comment`, `filename` and similar within strings. I didn't reliaze I could use `**kwargs` to store multiple named arguments. 71 | - renamed `file_name` to `filename`. 72 | - converted the rules to a dict json format. It kind of sucks not being able to cut and paste from the command line to the rules file but it didn't work with the named arguments. 73 | - only displays matches, not rules being scanned 74 | - I'll start uploading rules once this rule set format seems to be working well. 75 | - Thanks to OA for the feedback. -------------------------------------------------------------------------------- /hansel.py: -------------------------------------------------------------------------------- 1 | """ 2 | author: alexander hanel 3 | version: 2.2 4 | date: 2019-07-11 5 | 6 | """ 7 | 8 | import idautils 9 | import idaapi 10 | import datetime 11 | import glob 12 | import yara 13 | import operator 14 | import itertools 15 | import inspect 16 | import os 17 | import sys 18 | import json 19 | 20 | DEBUG = False 21 | if DEBUG: 22 | import traceback 23 | INIT = False 24 | 25 | SEARCH_CASE = 4 26 | SEARCH_REGEX = 8 27 | SEARCH_NOBRK = 16 28 | SEARCH_NOSHOW = 32 29 | SEARCH_UNICODE = 64 30 | SEARCH_IDENT = 128 31 | SEARCH_BRK = 256 32 | 33 | RULES_DIR = "" 34 | 35 | 36 | class YaraIDASearch: 37 | def __init__(self): 38 | self.mem_results = "" 39 | self.mem_offsets = [] 40 | if not self.mem_results: 41 | self._get_memory() 42 | 43 | def _wowrange(self, start, stop, step=1): 44 | # source https://stackoverflow.com/a/1482502 45 | if step == 0: 46 | raise ValueError('step must be != 0') 47 | elif step < 0: 48 | proceed = operator.gt 49 | else: 50 | proceed = operator.lt 51 | while proceed(start, stop): 52 | yield start 53 | start += step 54 | 55 | def _get_memory(self): 56 | print "Status: Loading memory for Yara." 57 | result = "" 58 | segments_starts = [ea for ea in idautils.Segments()] 59 | offsets = [] 60 | start_len = 0 61 | for start in segments_starts: 62 | end = idc.get_segm_end(start) 63 | for ea in self._wowrange(start, end): 64 | result += chr(idc.Byte(ea)) 65 | offsets.append((start, start_len, len(result))) 66 | start_len = len(result) 67 | print "Status: Memory has been loaded." 68 | self.mem_results = result 69 | self.mem_offsets = offsets 70 | 71 | def _to_virtual_address(self, offset, segments): 72 | va_offset = 0 73 | for seg in segments: 74 | if seg[1] <= offset < seg[2]: 75 | va_offset = seg[0] + (offset - seg[1]) 76 | return va_offset 77 | 78 | def _init_sig(self, sig_type, pattern, sflag): 79 | if SEARCH_REGEX & sflag: 80 | signature = "/%s/" % pattern 81 | if SEARCH_CASE & sflag: 82 | # ida is not case sensitive by default but yara is 83 | pass 84 | else: 85 | signature += " nocase" 86 | if SEARCH_UNICODE & sflag: 87 | signature += " wide" 88 | elif sig_type == "binary": 89 | signature = " %s " % pattern 90 | elif sig_type == "text" and (SEARCH_REGEX & sflag) == False: 91 | signature = '"%s"' % pattern 92 | if SEARCH_CASE & sflag: 93 | pass 94 | else: 95 | signature += " nocase" 96 | # removed logic to check for ascii or wide, might as well do both. 97 | #if SEARCH_UNICODE & sflag: 98 | signature += " wide ascii" 99 | yara_rule = "rule foo : bar { strings: $a = %s condition: $a }" % signature 100 | return yara_rule 101 | 102 | def _compile_rule(self, signature): 103 | try: 104 | rules = yara.compile(source=signature) 105 | except Exception as e: 106 | print "ERROR: Cannot compile Yara rule %s" % e 107 | return False, None 108 | return True, rules 109 | 110 | def _search(self, signature): 111 | status, rules = self._compile_rule(signature) 112 | if not status: 113 | return False, None 114 | values = [] 115 | matches = rules.match(data=self.mem_results) 116 | if not matches: 117 | return False, None 118 | for rule_match in matches: 119 | for match in rule_match.strings: 120 | match_offset = match[0] 121 | values.append(self._to_virtual_address(match_offset, self.mem_offsets)) 122 | return values 123 | 124 | def find_binary(self, bin_str, sflag=0): 125 | yara_sig = self._init_sig("binary", bin_str, sflag) 126 | offset_matches = self._search(yara_sig) 127 | return offset_matches 128 | 129 | def find_text(self, q_str, sflag=0): 130 | yara_sig = self._init_sig("text", q_str, sflag) 131 | offset_matches = self._search(yara_sig) 132 | return offset_matches 133 | 134 | def reload_scan_memory(self): 135 | self._get_memory() 136 | 137 | 138 | def is_lib(ea): 139 | """ 140 | is function a library 141 | :param ea: 142 | :return: if lib return True else return False 143 | """ 144 | flags = idc.get_func_attr(ea, FUNCATTR_FLAGS) 145 | if flags & FUNC_LIB: 146 | return True 147 | else: 148 | return False 149 | 150 | 151 | def get_func_symbols(ea): 152 | """ 153 | get all symbol/api calls from a function 154 | :param ea: offset within a function 155 | :return: return list of symbol/api 156 | """ 157 | offsets = [] 158 | dism_addr = list(idautils.FuncItems(ea)) 159 | for addr in dism_addr: 160 | if ida_idp.is_call_insn(addr): 161 | op_type = idc.get_operand_type(addr, 0) 162 | if op_type == 1: 163 | temp = idc.generate_disasm_line(addr, 0) 164 | # hack to extract api name if added as a comment to call register 165 | # sadly, idaapi.is_tilcmt isn't populated for api names 166 | if ";" in temp: 167 | temp_name = temp.split(";")[-1].strip() 168 | if idc.get_name_ea_simple(temp_name) and "@" not in temp_name: 169 | offsets.append((addr, temp_name)) 170 | else: 171 | continue 172 | elif op_type == 2: 173 | temp_name = Name(idc.get_operand_value(addr, 0)) 174 | if "@" not in temp_name: 175 | offsets.append((addr, temp_name)) 176 | else: 177 | op_addr = idc.get_operand_value(addr, 0) 178 | if is_lib(op_addr): 179 | temp_name = idc.get_func_name(op_addr) 180 | if "@" not in temp_name: 181 | offsets.append((addr, temp_name)) 182 | return offsets 183 | 184 | 185 | def get_func_str_hack(ea): 186 | """ 187 | get all referenced strings within a function, actually works 188 | :param ea: offset within a function 189 | :return: return list of strings referenced in function 190 | """ 191 | offsets = [] 192 | status, ea_st = get_func_addr(ea) 193 | if status: 194 | status, ea_end = get_func_addr_end(ea) 195 | if status: 196 | for _str in idautils.Strings(): 197 | s_ea = _str.ea 198 | xref = idautils.XrefsTo(s_ea) 199 | for x in xref: 200 | temp_addr = x.frm 201 | if ea_st <= temp_addr <= ea_end: 202 | offsets.append((temp_addr, _str)) 203 | return offsets 204 | 205 | 206 | def get_func_strings(ea): 207 | """ 208 | get all referenced strings within a function, doesn't really work well 209 | :param ea: offset within a function 210 | :return: return list of strings referenced in a a function 211 | """ 212 | offsets = [] 213 | dism_addr = list(idautils.FuncItems(ea)) 214 | for addr in dism_addr: 215 | idaapi.decode_insn(addr) 216 | for count, op in enumerate(idaapi.cmd.Operands): 217 | # print count, op.type, hex(addr)[:-1], hex(idc.get_operand_value(addr, count)) 218 | if op.type == idaapi.o_void: 219 | break 220 | if op.type == idaapi.o_imm or op.type == idaapi.o_mem: 221 | val_addr = idc.get_operand_value(addr, count) 222 | temp_str = idc.get_strlit_contents(val_addr) 223 | if temp_str: 224 | if val_addr not in dism_addr and get_func_name(val_addr) == "": 225 | offsets.append((addr, temp_str)) 226 | return offsets 227 | 228 | 229 | def get_func_values(ea): 230 | """ 231 | get all integer values within a function 232 | :param ea: offset within a function 233 | :return: return list of integer values within a function 234 | """ 235 | offsets = [] 236 | dism_addr = list(idautils.FuncItems(ea)) 237 | for addr in dism_addr: 238 | idaapi.decode_insn(addr) 239 | for c, v in enumerate(idaapi.cmd.Operands): 240 | if v.type == idaapi.o_void: 241 | break 242 | if v.type == idaapi.o_imm: 243 | value = idc.get_operand_value(addr, c) 244 | if not is_loaded(value): 245 | offsets.append((addr, value)) 246 | if v.type == idaapi.o_displ: 247 | value = idc.get_operand_value(addr, c) 248 | offsets.append((addr, value)) 249 | return offsets 250 | 251 | 252 | def generate_skeleton(ea): 253 | """ 254 | auto generate all attributes from a function that can be used for rule creation 255 | :param ea: offset within a function 256 | :return: return auto generated rule (likely needs to be edited) 257 | """ 258 | skeleton = set([]) 259 | status, ea = get_func_addr(ea) 260 | if status: 261 | for x in get_func_symbols(ea): 262 | skeleton.add("%s" % x[1]) 263 | for x in get_func_str_hack(ea): 264 | skeleton.add("%s" % x[1]) 265 | for x in get_func_strings(ea): 266 | skeleton.add("%s" % x[1]) 267 | for x in get_func_values(ea): 268 | skeleton.add(int(x[1])) 269 | return list(skeleton) 270 | 271 | 272 | def get_xrefsto(ea): 273 | """ 274 | TODO 275 | :param ea: 276 | :return: 277 | """ 278 | if ea: 279 | return [x.frm for x in idautils.XrefsTo(ea, 1)] 280 | else: 281 | return [] 282 | 283 | 284 | def get_func_addr(ea): 285 | """ 286 | get function offset start 287 | :param ea: address 288 | :return: returns offset of the start of the function 289 | """ 290 | if ea: 291 | tt = idaapi.get_func(ea) 292 | if tt: 293 | return True, tt.startEA 294 | return False, None 295 | 296 | 297 | def get_func_addr_end(ea): 298 | """ 299 | get funtion offset end 300 | :param ea: address 301 | :return: returns offset of the end of the function 302 | """ 303 | tt = idaapi.get_func(ea) 304 | if tt: 305 | return True, tt.end_ea 306 | return False, None 307 | 308 | 309 | def func_xref_api_search(offset_list, api_list): 310 | """ 311 | hmm apparently this isn't needed 312 | :param offset_list: 313 | :param api_list: 314 | :return: 315 | """ 316 | matches = [] 317 | for offset in offset_list: 318 | xref_offset = get_xrefsto(offset) 319 | for xref_offset in xref_offset: 320 | func_calls = get_func_symbols(xref_offset) 321 | api_name = [x[1] for x in func_calls] 322 | if set(api_list).issubset(api_name): 323 | matches.append(idc.get_func_name(xref_offset)) 324 | return matches 325 | 326 | 327 | def search_binary(query): 328 | """ 329 | search using yara patterns 330 | """ 331 | global yara_search 332 | match = yara_search.find_binary(query) 333 | if match: 334 | func_match = [] 335 | for offset in match: 336 | offset_xref = get_xrefsto(offset) 337 | if offset_xref: 338 | [func_match.append(x) for x in offset_xref] 339 | else: 340 | func_match.append(offset) 341 | if func_match: 342 | return True, func_match 343 | return False, None 344 | 345 | 346 | def search_string(query): 347 | """ 348 | search string, check if Name or string is present 349 | :param query: 350 | :return: 351 | """ 352 | global yara_search 353 | name_offset = idc.get_name_ea_simple(query) 354 | if name_offset != BADADDR: 355 | match = get_xrefsto(name_offset) 356 | if match: 357 | func_match = match 358 | return True, func_match 359 | match = yara_search.find_text(query) 360 | if match: 361 | func_match = [] 362 | for offset in match: 363 | offset_xref = get_xrefsto(offset) 364 | [func_match.append(x) for x in offset_xref] 365 | if func_match: 366 | return True, func_match 367 | return False, None 368 | 369 | 370 | def search_value(value_list, dict_match): 371 | """ 372 | search if value exists in function returns str of list 373 | :param value_list: list of values to search for 374 | :param dict_match: 375 | :return: (Status, Matches) 376 | """ 377 | func_addr = [] 378 | if dict_match: 379 | temp_list = [[i for i in dict_match[kk]] for kk in dict_match.keys()] 380 | xref_offset = set(itertools.chain(*temp_list)) 381 | for xref in xref_offset: 382 | status, offset = get_func_addr(xref) 383 | if status: 384 | func_addr.append(offset) 385 | else: 386 | func_addr = list(idautils.Functions()) 387 | for func in func_addr: 388 | temp_func_values = set([x[1] for x in get_func_values(func)]) 389 | if set(value_list).issubset(temp_func_values): 390 | for v in value_list: 391 | if v not in dict_match: 392 | dict_match[v] = set([func]) 393 | else: 394 | dict_match[v].add(func) 395 | if dict_match: 396 | return True, dict_match 397 | return False, None 398 | 399 | 400 | def search(*search_terms, **kwargs): 401 | """ 402 | 403 | :param search_terms: tuple of strings, integers, API/Symbols, etc to search for 404 | :return: tuple(Status, List) Status could be True or False, List of function matches offset 405 | """ 406 | dict_match = {} 407 | value_list = [] 408 | temp_comment = False 409 | temp_context = False 410 | temp_rename = False 411 | temp_file = False 412 | if "comment" in kwargs.keys(): 413 | temp_comment = kwargs["comment"] 414 | if "rename" in kwargs.keys(): 415 | temp_rename = kwargs["rename"] 416 | if temp_rename == False and "context" in kwargs.keys(): 417 | temp_comment = kwargs["context"] 418 | if "context" in kwargs.keys(): 419 | temp_context = kwargs["context"] 420 | if "filename" in kwargs.keys(): 421 | temp_file = kwargs["filename"] 422 | # start search 423 | status = False 424 | for term in search_terms: 425 | if isinstance(term, str): 426 | # start yara search 427 | if term.startswith("{"): 428 | status, yara_results = search_binary(term) 429 | if not status: 430 | return False, None 431 | else: 432 | for ea in yara_results: 433 | status, offset = get_func_addr(ea) 434 | if status: 435 | if term not in dict_match: 436 | dict_match[term] = [offset] 437 | else: 438 | dict_match[term].append(offset) 439 | # single yara byte pattern search 440 | if len(search_terms) == 1 and yara_results[0] and status == False: 441 | label_binary(yara_results, temp_comment) 442 | return True, yara_results 443 | 444 | else: 445 | # start string search 446 | status, string_results = search_string(term) 447 | if not status: 448 | return False, None 449 | else: 450 | for ea in string_results: 451 | status, offset = get_func_addr(ea) 452 | if status: 453 | if term not in dict_match: 454 | dict_match[term] = [offset] 455 | else: 456 | dict_match[term].append(offset) 457 | elif isinstance(term, int) or isinstance(term, long) : 458 | value_list.append(term) 459 | # start integer search 460 | if value_list: 461 | if DEBUG: 462 | print "value_list %s" % value_list 463 | status, temp_match = search_value(value_list, dict_match) 464 | if status: 465 | dict_match = temp_match 466 | if DEBUG: 467 | print dict_match 468 | # cross-reference matches to a single function 469 | if dict_match: 470 | if len(dict_match.keys()) == len(search_terms): 471 | func_list = [set(dict_match[key]) for key in dict_match.keys()] 472 | if len(search_terms) == 1: 473 | label_(func_list[0], temp_comment, temp_rename) 474 | return True, func_list[0] 475 | func_match = set.intersection(*func_list) 476 | if func_match: 477 | label_(func_match, temp_comment, temp_rename) 478 | return True, func_match 479 | return False, None 480 | 481 | 482 | def label_(func_match, temp_comment, temp_rename): 483 | """ 484 | adds comment or renames function 485 | :param func_match: function offset 486 | :param temp_comment: string comment 487 | :param temp_rename: string function name 488 | :return: 489 | """ 490 | for match in func_match: 491 | if temp_comment: 492 | comm_func(match, temp_comment) 493 | if temp_rename: 494 | name_func(match, temp_rename) 495 | 496 | 497 | def name_func(ea, name): 498 | """ 499 | Rename a function, appends string if already renamed 500 | :param ea: start offset to a function 501 | :param name: 502 | :return: 503 | TODO check warnings and increment if name is present 504 | """ 505 | f = idc.get_full_flags(ea) 506 | if not idc.hasUserName(f): 507 | idc.set_name(ea, name, SN_CHECK) 508 | else: 509 | temp = idc.get_name(ea) 510 | # do not rename WinMain 511 | if name in temp or "winmain" in temp.lower(): 512 | return 513 | temp_name = temp + "_" + name 514 | idc.set_name(ea, temp_name, SN_CHECK) 515 | 516 | 517 | def comm_func(ea, comment): 518 | """ 519 | Add function comment 520 | :param ea: start offset to a function 521 | :param comment: string of comment to add 522 | :return: None 523 | """ 524 | temp = idc.get_func_cmt(ea, True) 525 | if comment in temp: 526 | return 527 | if temp: 528 | tt = temp + " " + comment 529 | idc.set_func_cmt(ea, tt, True) 530 | else: 531 | idc.set_func_cmt(ea, comment, True) 532 | 533 | 534 | def label_binary(yara_match, comment): 535 | if comment: 536 | for ea in yara_match: 537 | temp = idc.get_cmt(ea, True) 538 | if temp: 539 | if comment in temp: 540 | continue 541 | tt = temp + " " + comment 542 | idc.set_cmt(ea, tt, True) 543 | else: 544 | idc.set_cmt(ea, comment, True) 545 | 546 | 547 | def save_search(*search_terms, **kwargs): 548 | """ 549 | save search to a file (specified with `file_name=FILENAME`) 550 | :param search_terms: search string. 551 | :return: None 552 | """ 553 | temp_rule = "" 554 | if "filename" in kwargs.keys(): 555 | temp_rule = kwargs["filename"] 556 | kwargs.pop("filename", None) 557 | save = {} 558 | save["search_terms"] = search_terms 559 | save["kwargs"] = kwargs 560 | rule_path = get_rules_dir() 561 | if temp_rule: 562 | file_name = temp_rule 563 | temp_name = os.path.join(rule_path, file_name) 564 | if os.path.exists(temp_name): 565 | with open(str(temp_name), "a+") as f_h: 566 | f_h.write(json.dumps(save)) 567 | f_h.write("\n") 568 | else: 569 | with open(temp_name, "w") as f_h: 570 | f_h.write(json.dumps(save)) 571 | f_h.write("\n") 572 | else: 573 | print 'ERROR: Must supply argument with file name filename="FOO.rule"' 574 | 575 | 576 | def add_hotkey(): 577 | """ 578 | enable hotkey of ALT-/ 579 | """ 580 | ida_kernwin.add_hotkey("Alt-/", hotkey_rule) 581 | 582 | 583 | def hotkey_rule(): 584 | """ 585 | create rule using date as file name using the current function as the input for the skelton rule 586 | :return: 587 | """ 588 | # get skelton 589 | ea = here() 590 | skeleton = generate_skeleton(ea) 591 | save = {} 592 | save["search_terms"] = skeleton 593 | # get context 594 | function_addr = "0x%x" % (get_func_addr(ea)[1]) 595 | context = "%s, %s" % (idc.get_idb_path(), function_addr) 596 | save["kwargs"] = {"context" : context} 597 | # get path and create file name 598 | rule_path = get_rules_dir() 599 | temp_name = str(datetime.datetime.now().strftime("%Y-%m-%d")) + ".rule" 600 | file_path = os.path.join(rule_path, temp_name) 601 | if os.path.exists(file_path): 602 | with open(str(file_path), "a+") as f_h: 603 | f_h.write(json.dumps(save)) 604 | f_h.write("\n") 605 | else: 606 | with open(file_path, "w") as f_h: 607 | f_h.write(json.dumps(save)) 608 | f_h.write("\n") 609 | 610 | 611 | def get_rules_dir(): 612 | """ 613 | helper function that gets the rule directory 614 | :return: string of the path to the rule directory 615 | """ 616 | if RULES_DIR: 617 | return RULES_DIR 618 | else: 619 | return os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), "rules") 620 | 621 | 622 | def run_rules(): 623 | """ 624 | run search using all rules in the rule directory 625 | :return: None 626 | """ 627 | rule_path = get_rules_dir() 628 | paths = glob.glob(rule_path + "\*") 629 | for path in paths: 630 | if os.path.isdir(path): 631 | continue 632 | with open(path, "r") as rule: 633 | for line_search in rule.readlines(): 634 | try: 635 | # convert unicode to ascii 636 | saved_rule = byteify(json.loads(line_search)) 637 | rule = saved_rule["search_terms"] 638 | kwarg = saved_rule["kwargs"] 639 | status, match = search(*rule,**kwarg) 640 | if status: 641 | for m in match: 642 | print "RULE(s): %s" % rule_path 643 | print "\tSEARCH: %s" % rule 644 | print "\tMatch at 0x%x" % m 645 | except Exception as e: 646 | print "ERROR: Review file %s rule %s, %s" % (path, line_search.rstrip(), e) 647 | 648 | 649 | def run_rule(rule_name): 650 | """ 651 | search using a single file 652 | :param rule_name: string file name to save rule to 653 | :return: None 654 | """ 655 | rule_dir = get_rules_dir() 656 | rule_path = os.path.join(rule_dir, rule_name) 657 | if os.path.isfile(rule_path): 658 | with open(rule_path, "r") as rule: 659 | for line_search in rule.readlines(): 660 | try: 661 | # convert unicode to ascii 662 | saved_rule = byteify(json.loads(line_search)) 663 | rule = saved_rule["search_terms"] 664 | kwarg = saved_rule["kwargs"] 665 | status, match = search(*rule,**kwarg) 666 | if status: 667 | for m in match: 668 | print "RULE(s): %s" % rule_path 669 | print "\tSEARCH: %s" % rule 670 | print "\tMatch at 0x%x" % m 671 | except Exception as e: 672 | print "ERROR: Review file %s rule %s, %s" % (rule_path, line_search.rstrip(), e) 673 | if DEBUG: 674 | print traceback.format_exc() 675 | 676 | else: 677 | print "ERROR: File %s could not be found" 678 | 679 | 680 | def cheat_sheet(): 681 | print """ 682 | search("query1", "query2", comment="My_Comment", rename="FUNCTION_NAME") 683 | save_search( "query1",file_name="RULE_NAME.rule", comment="My_Comment", rename="FUNCTION_NAME") 684 | run_rule("RULES_NAME.rule") 685 | run_rules() <- no arguments 686 | hot_key() <- saves output of generate_skelton(ea) to rules directory with the date as the name 687 | added by hot_key() context="XYZ.idb, 0x40000 = func offset" 688 | """ 689 | 690 | 691 | def byteify(input): 692 | # source https://stackoverflow.com/a/13105359 693 | if isinstance(input, dict): 694 | return {byteify(key): byteify(value) 695 | for key, value in input.iteritems()} 696 | elif isinstance(input, list): 697 | return [byteify(element) for element in input] 698 | elif isinstance(input, unicode): 699 | return input.encode('utf-8') 700 | else: 701 | return input 702 | 703 | 704 | if not INIT: 705 | yara_search = YaraIDASearch() 706 | add_hotkey() 707 | INIT = True 708 | 709 | -------------------------------------------------------------------------------- /rules/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore --------------------------------------------------------------------------------