├── README.md └── LazyIDA.py /README.md: -------------------------------------------------------------------------------- 1 | # LazyIDA 2 | Xem hướng dẫn, hình ảnh ở: https://github.com/L4ys/LazyIDA 3 | 4 | Chịu khó đọc code, và thử right-click trong IDA, tui đã add rất nhiều tính năng thường dùng hàng ngày trên IDA vào. 5 | 6 | Làm biếng liệt kê ra 7 | 8 | # Hướng dẫn sử dụng viết sau 9 | 10 | Cảm ơn dùng thử, vui lòng report bugs nếu có. 11 | 12 | Chân chọng 13 | 14 | HTC 15 | -------------------------------------------------------------------------------- /LazyIDA.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0301,C0103,C0111 2 | 3 | # 4 | # License: Beerware license ;) 5 | # 6 | # Link update: 7 | # https://github.com/HongThatCong/LazyIDA 8 | # Origin version: 9 | # https://github.com/L4ys/LazyIDA 10 | # 11 | # Version histories: 12 | # 09/08/2020 - HTC 13 | # - fix bug read_selection 14 | # - fix bug parse_location, so Shift-G hotkey can jumpto any ea, name possibles 15 | # - change "C" hotkey to Shift-C, because C is duplicate function with IDA C and Ctrl-C hotkey 16 | # Shift-C will copy full name of a current highlight name 17 | # - add "Shfit-V" hotkey, paste name from clipboard to current highlight name or current addr (fullname) 18 | # 10/08/2020 19 | # - Fix bug sanitize name from clipboard text - idea of Ngon Nguyen, code by HTC 20 | # 30/08/2020 21 | # - Rearrange actions and hotkeys - HTC 22 | # - Refactor, clean code, add nhieu tinh nang. Nhieu qua, lam bieng liet ke, chiu kho xem code hay diff :D 23 | # 31/01/2021 24 | # - Ver 1.0.5 25 | # 20/12/2021: 26 | # - Add Revert IDA Decision 27 | # - Ver 1.0.6 28 | # 26/12/2022: 29 | # - Port to Python 3 30 | # - Ver 1.1 31 | # - Add Shift-X, display refs to a register 32 | # - Flake8, refactor, fix some bugs 33 | # 34 | # This script is licensed under the "THE BEER-WARE LICENSE" (Revision 42) license :D 35 | # 36 | 37 | from __future__ import division 38 | from __future__ import print_function 39 | 40 | import os 41 | import string 42 | import re 43 | import base64 44 | from urllib.parse import quote 45 | 46 | from struct import unpack 47 | 48 | import idaapi 49 | import idautils 50 | import idc 51 | import ida_kernwin 52 | import ida_xref 53 | import ida_name 54 | import ida_loader 55 | 56 | from PyQt5.Qt import QApplication 57 | 58 | PLUGIN_NAME = "LazyIDA" 59 | PLUGIN_VERSION = "1.1" 60 | PLUGIN_POPUP = "LazyIDA/" 61 | 62 | # Popup menus action names 63 | ACTION_MENU_CONVERT = [f"lazyida:convert_{i}" for i in range(12)] 64 | ACTION_MENU_SCAN_VUL = "lazyida:scan_vul" 65 | ACTION_MENU_COPY_DATA = "lazyida:copy_data" # added by merc 66 | ACTION_MENU_COPY_STR = "lazyida:copystring" # HTC 67 | ACTION_MENU_DUMP_DATA = "lazyida:dump_data" # HTC 68 | ACTION_MENU_DUMP_SEG = "lazyida:dump_seg" 69 | ACTION_MENU_XOR_DATA = "lazyida:xor_data" # HTC & CatBui mod 70 | ACTION_MENU_FILL_NOP = "lazyida:fill_nop" 71 | ACTION_MENU_NOP_HIDER = "lazyida:nop_hider" # added by HTC 72 | ACTION_MENU_AUTO_OFF = "lazyida:turn_off_ida_decision" 73 | ACTION_MENU_B64STD = "lazyida:base64std_decode" 74 | ACTION_MENU_B64URL = "lazyida:base64url_decode" 75 | 76 | # 77 | # HTC: change actions name and hotkeys - 30/08/2020 78 | # Available Shift- hotkeys: B C G H J K O Q V T Y Z 79 | # All actions name and hotkeys, enable allways 80 | # Action is tuple: name, label, shortcut, tooltip/hint, iconid 81 | # 82 | ACTION_HOTKEY_COPY_EA = ("lazyida:copy_ea", "Copy EA", "Shift-Y", "Copy current EA to clipboard", 0x1F) 83 | ACTION_HOTKEY_COPY_RVA = ("lazyida:copy_rva", "Copy RVA", "Shift-Z", "Copy current RVA to clipboard", 0x1F) 84 | ACTION_HOTKEY_COPY_FOFS = ("lazyida:copy_fofs", "Copy File Offset", "Shift-O", "Copy current file offset to clipboard", 0x1F) 85 | ACTION_HOTKEY_COPY_NAME = ("lazyida:copy_name", "Copy highligh name", "Shift-C", "Copy current highlight full name to clipboard", 0x1F) 86 | ACTION_HOTKEY_PASTE_NAME = ("lazyida:paste_name", "Paste to highligh name", "Shift-V", "Change the fullname of current highlight to clipboard text", 0x13) 87 | ACTION_HOTKEY_GOTO_CLIP = ("lazyida:goto_clip", "Goto text in clipboard", "Shift-G", "Goto current name or ea in clipboard", 0x7D) 88 | ACTION_HOTKEY_GOTO_FOFS = ("lazyida:goto_fofs", "Goto file offset", "Shift-J", "Goto file offset", 0x7D) # IDA already had this action "JumpFileOffset" 89 | ACTION_HOTKEY_GOTO_RVA = ("lazyida:goto_rva", "Goto RVA", "Alt-G", "Goto RVA", 0x7D) 90 | ACTION_HOTKEY_SEARCH_GOOGLE = ("lazyida:search_google", "Search Google", "Ctrl-Shift-G", "Search Google", 0x21) 91 | ACTION_HOTKEY_SEARCH_MSDOC = ("lazyida:search_msdoc", "Search MS Docs", "Ctrl-Shift-S", "Search MS Docs", 0x21) 92 | ACTION_HOTKEY_SEARCH_BING = ("lazyida:search_bing", "Search Bing", "Ctrl-Shift-J", "Search Bing", 0x21) 93 | ACTION_HOTKEY_SEARCH_GITHUB = ("lazyida:search_github", "Search Github", "Ctrl-Shift-H", "Search Github", 0x21) 94 | ACTION_HOTKEY_OPEN_URL = ("lazyida:open_url", "Open URL", "", "Open URL", 0) 95 | 96 | ALL_HOTKEY_ACTIONS = ( 97 | ACTION_HOTKEY_COPY_EA, 98 | ACTION_HOTKEY_COPY_RVA, 99 | ACTION_HOTKEY_COPY_FOFS, 100 | ACTION_HOTKEY_COPY_NAME, 101 | ACTION_HOTKEY_PASTE_NAME, 102 | ACTION_HOTKEY_GOTO_CLIP, 103 | ACTION_HOTKEY_GOTO_FOFS, 104 | ACTION_HOTKEY_GOTO_RVA, 105 | ACTION_HOTKEY_SEARCH_GOOGLE, 106 | ACTION_HOTKEY_SEARCH_MSDOC, 107 | ACTION_HOTKEY_SEARCH_BING, 108 | ACTION_HOTKEY_SEARCH_GITHUB, 109 | ACTION_HOTKEY_OPEN_URL, 110 | ) 111 | 112 | # Decompiler view hotkeys 113 | # HTC: V duplicate with HexCodeXplorer plugin ctree_item_vew, so change to Alt-R 114 | ACTION_HX_REMOVE_RET_TYPE = ("lazyida::hx_removerettype", "Remove return type", "Alt-R", "Set return type of current function to void") 115 | 116 | 117 | def calc_lazy_bits(): 118 | info = idaapi.get_inf_structure() 119 | if info.is_64bit(): 120 | return 64 121 | elif info.is_32bit(): 122 | return 32 123 | else: 124 | return 16 125 | 126 | 127 | LAZY_ARCH = idaapi.ph_get_id() 128 | LAZY_BITS = calc_lazy_bits() 129 | 130 | IS_BE = idaapi.get_inf_structure().is_be() 131 | CAN_NOP = LAZY_ARCH == idaapi.PLFM_386 132 | ENABLE_REMOVE_RETTYPE = "hx:RemoveArg" not in idaapi.get_registered_actions() # HexRays 7.5+ 133 | 134 | 135 | def u16(x): 136 | return unpack(" 168 | <##Size :{intSize}> 169 | <##End EA (not included):{intEndEA}> 170 | """, 171 | { 172 | 'intStartEA': idaapi.Form.NumericInput(swidth=20, tp=idaapi.Form.FT_HEX, value=start_ea), 173 | 'intSize': idaapi.Form.NumericInput(swidth=20, tp=idaapi.Form.FT_HEX, value=end_ea - start_ea), 174 | 'intEndEA': idaapi.Form.NumericInput(swidth=20, tp=idaapi.Form.FT_HEX, value=end_ea), 175 | 'FormChangeCb': idaapi.Form.FormChangeCb(self.OnFormChange), 176 | }) 177 | 178 | def OnFormChange(self, fid): 179 | # Set initial state 180 | if fid == -1: 181 | self.EnableField(self.intEndEA, False) 182 | 183 | start = self.GetControlValue(self.intStartEA) 184 | size = self.GetControlValue(self.intSize) 185 | if start and size: 186 | self.SetControlValue(self.intEndEA, start + size) 187 | 188 | return 1 189 | 190 | 191 | def plg_print(smsg): 192 | print(f"[{PLUGIN_NAME}] {smsg}") 193 | 194 | 195 | def is_valid_addr(ea): 196 | return idc.get_inf_attr(idc.INF_MIN_EA) <= ea <= idc.get_inf_attr(idc.INF_MAX_EA) 197 | 198 | 199 | def parse_location(text): 200 | """Parse text to hex ea or try to get a valid name""" 201 | plg_print(f'Clipboard text is "{text}"') 202 | 203 | if not text: 204 | return idaapi.BADADDR 205 | 206 | strs = re.findall(r"[\da-f]+", text, re.IGNORECASE) # parse hex number 207 | if strs: 208 | for s in strs: 209 | try: 210 | ea = int(s, 16) 211 | if is_valid_addr(ea): 212 | return ea 213 | except ValueError: 214 | pass 215 | 216 | # Fail to find a valid ea, assume text is a name 217 | ea = idc.get_name_ea_simple(text) 218 | if is_valid_addr(ea): 219 | return ea 220 | 221 | # Try to sanitize the name 222 | san = ida_name.validate_name(text, ida_name.VNT_IDENT) 223 | if san: 224 | ea = idc.get_name_ea_simple(san) 225 | if is_valid_addr(ea): 226 | return ea 227 | 228 | # Parse text into words, assume every words is a valid name 229 | strs = re.findall(r"\w+", text) 230 | if strs: 231 | for s in strs: 232 | ea = idc.get_name_ea_simple(s) 233 | if is_valid_addr(ea): 234 | return ea 235 | 236 | return idaapi.BADADDR 237 | 238 | 239 | def lazy_read_selection(): 240 | # HTC - Ignore the byte at end address 241 | sel, start, end = idaapi.read_range_selection(None) 242 | if not sel: 243 | if idc.get_item_size(idc.get_screen_ea()): 244 | start = idc.get_screen_ea() 245 | end = start + idc.get_item_size(start) 246 | sel = True 247 | 248 | if not sel: 249 | start = idaapi.BADADDR 250 | end = idaapi.BADADDR 251 | else: 252 | rFrm = RangeForm(start, end) 253 | rFrm.Compile() 254 | ok = rFrm.Execute() 255 | if ok == 1: 256 | # OK 257 | start = rFrm.intStartEA.value 258 | end = start + rFrm.intSize.value 259 | else: 260 | # Cancel 261 | sel = False 262 | 263 | rFrm.Free() 264 | 265 | # Ensure we have at least one byte 266 | if end <= start: 267 | sel = False 268 | 269 | return sel, start, end 270 | 271 | 272 | def goto_clip_text(): 273 | loc = parse_location(clip_text()) 274 | if loc != idaapi.BADADDR: 275 | plg_print(f"Goto location 0x{loc:X}") 276 | idc.jumpto(loc) 277 | return 1 278 | 279 | plg_print("Failed to get a valid ea") 280 | return 0 281 | 282 | 283 | def str2hex(txt): 284 | if not txt: 285 | return 0 286 | 287 | # is it a valid number ? 288 | val = 0 289 | if txt.endswith("h"): 290 | txt = txt[:-1] 291 | try: 292 | val = int(txt, 16) 293 | except ValueError: 294 | pass 295 | 296 | return val 297 | 298 | 299 | def get_number_from_highlight(): 300 | view = idaapi.get_current_viewer() 301 | thing = ida_kernwin.get_highlight(view) 302 | if thing and thing[1]: 303 | # we have a highlight 304 | val = str2hex(thing[0]) 305 | if val != 0: 306 | # have a valid hex number 307 | return val 308 | 309 | return str2hex(clip_text()) 310 | 311 | 312 | def get_screen_module(): 313 | if not idaapi.is_debugger_on(): 314 | return (idaapi.get_imagebase(), idaapi.get_input_file_path()) 315 | else: 316 | ea = idc.get_screen_ea() 317 | mod = idaapi.modinfo_t() 318 | while mod: 319 | if mod.base <= ea < mod.base + mod.size: 320 | return (mod.base, mod.name) 321 | if not idaapi.get_next_module(mod): 322 | break 323 | return idaapi.BADADDR 324 | 325 | 326 | def goto_rva(): 327 | rva = get_number_from_highlight() 328 | rva = ida_kernwin.ask_addr(rva, "Enter the RVA to jump to") 329 | if rva is None: 330 | return 0 331 | 332 | base, _ = get_screen_module() 333 | ea = base + rva 334 | plg_print(f"Base = 0x{base:X}, RVA = 0x{rva:X} => EA = 0x{ea:X}") 335 | idc.jumpto(ea) 336 | return 1 337 | 338 | 339 | def goto_file_ofs(): 340 | fofs = get_number_from_highlight() 341 | fofs = ida_kernwin.ask_addr(fofs, "Enter the file offset to jump to") 342 | if fofs is None: 343 | return 0 344 | 345 | ea = idaapi.get_fileregion_ea(fofs) 346 | if ea != idc.BADADDR: 347 | plg_print(f"File offset = 0x{fofs:X} -> EA = 0x{ea:X}") 348 | idc.jumpto(ea) 349 | return 1 350 | else: 351 | plg_print(f"Could not goto file offset 0x{fofs:X}") 352 | return 0 353 | 354 | 355 | def copy_rva(): 356 | ea = idc.get_screen_ea() 357 | base, _ = get_screen_module() 358 | rva = ea - base 359 | plg_print(f"EA = 0x{ea:X}, Base = 0x{base:X} => RVA = 0x{rva:X} copied to clipboard") 360 | copy_to_clip(f"0x{rva:X}") 361 | 362 | 363 | def copy_file_offset(): 364 | ea = idc.get_screen_ea() 365 | fofs = ida_loader.get_fileregion_offset(ea) 366 | if fofs == -1: 367 | plg_print("File offset unknown") 368 | return 0 369 | 370 | plg_print(f"EA = 0x{ea:X} -> file offset = 0x{fofs:X} copied to clipboard") 371 | copy_to_clip(f"0x{fofs:X}") 372 | return 1 373 | 374 | 375 | # Org code of William Ballethin (FireEye) - hints-call plugin 376 | # Thanks Willi :) 377 | def get_ea_from_highlight(): 378 | view = idaapi.get_current_viewer() 379 | thing = ida_kernwin.get_highlight(view) 380 | if thing and thing[1]: 381 | # we have a highligh, is it a valid name ? 382 | ea = idc.get_name_ea_simple(thing[0]) 383 | if ea != idaapi.BADADDR: 384 | return ea 385 | 386 | # get name at screen ea 387 | ea = idc.get_screen_ea() 388 | name = idc.get_name(ea, idaapi.GN_DEMANGLED) 389 | if name and thing[0] in name: 390 | return ea 391 | 392 | # are we at end of function ? 393 | fn = idaapi.get_func(ea) 394 | if fn: 395 | if ea == fn.end_ea or idc.next_head(ea) == fn.end_ea: 396 | name = idc.get_name(fn.start_ea, idaapi.GN_DEMANGLED) 397 | if name and thing[0] in name: 398 | return fn.start_ea 399 | 400 | # Try to get full highlight name 401 | place = idaapi.get_custom_viewer_place(view, False) 402 | if place and len(place) == 3: # (plate_t, x, y) 403 | ea = place[0].toea() 404 | far_code_refs = [xref.to for xref in idautils.XrefsFrom(ea, ida_xref.XREF_FAR)] 405 | if far_code_refs: 406 | return far_code_refs[0] # First xref 407 | 408 | # Reach now, we do not have any valid name, return current screen ea 409 | return idc.get_screen_ea() 410 | 411 | 412 | def copy_highlight_name(): 413 | ea = get_ea_from_highlight() 414 | if ea != idaapi.BADADDR: 415 | name = idc.get_name(ea) 416 | if not name: 417 | name = f"0x{ea:X}" # copy ea 418 | copy_to_clip(name) 419 | plg_print(f"'{name}' copied to clipboard") 420 | return True 421 | else: 422 | plg_print("Invalid ea to copy") 423 | return False 424 | 425 | 426 | def paste_highlight_name(): 427 | ea = get_ea_from_highlight() 428 | if ea != idaapi.BADADDR: 429 | name = clip_text() 430 | if name: 431 | if not ida_name.force_name(ea, name): 432 | name = ida_name.validate_name(name, ida_name.VNT_IDENT) 433 | if not ida_name.force_name(ea, name): 434 | plg_print(f"FAILED to set name '{name}' to 0x{ea:X}") 435 | return False 436 | 437 | plg_print(f"Set name '{name}' to 0x{ea:X}") 438 | return True 439 | else: 440 | plg_print("Clipboard is empty") 441 | else: 442 | plg_print("Invalid ea to paste") 443 | 444 | return False 445 | 446 | 447 | def get_selected_text(): 448 | """ Get the highlight text. If none, force IDA copy text and we will get from clipboard """ 449 | text = "" 450 | old_text = clip_text() 451 | 452 | view = idaapi.get_current_viewer() 453 | if view: 454 | thing = ida_kernwin.get_highlight(view) 455 | if thing and thing[1]: 456 | text = thing[0] 457 | 458 | # We not have a highlight text 459 | if not text: 460 | for action in idaapi.get_registered_actions(): 461 | if "Copy" in action: 462 | shortcut = idaapi.get_action_shortcut(action) 463 | state = idaapi.get_action_state(action) 464 | if ("Ctrl-C" in shortcut) and (state and state[0] and (state[1] <= idaapi.AST_ENABLE)): 465 | idaapi.process_ui_action(action) 466 | text = clip_text() 467 | if text != old_text: 468 | break 469 | 470 | if not text: 471 | plg_print("Could not get any highlight/auto copied text\n" 472 | f"Search with old clipboard text: '{old_text}'") 473 | text = old_text 474 | 475 | return text 476 | 477 | 478 | def search_web(idx): 479 | urls = ["https://www.google.com/search?q=%s", 480 | "https://docs.microsoft.com/en-us/search/?terms=%s", 481 | "https://www.bing.com/search?q=%s", 482 | "https://github.com/search?q=%s&type=Code"] 483 | assert idx in range(len(urls)) 484 | 485 | text = get_selected_text() 486 | if text: 487 | copy_to_clip(text) 488 | idaapi.open_url(urls[idx] % quote(text)) 489 | 490 | 491 | def dump_data_to_file(fName, data): 492 | defPath = os.path.dirname(idaapi.get_input_file_path()) 493 | defPath = os.path.join(defPath, fName) 494 | dumpPath = idaapi.ask_file(1, defPath, "*.dump") 495 | if dumpPath: 496 | try: 497 | with open(dumpPath, "wb") as f: 498 | f.write(data) 499 | plg_print(f"Dump {len(data)} bytes to file {dumpPath} successed") 500 | except IOError as e: 501 | plg_print(str(e)) 502 | 503 | 504 | def process_data_result(start, data): 505 | # 16 bytes on a line 506 | # one byte take 4 char: 2 hex char, a space and a char if isalnum 507 | # one line take 3 char addtion: two space and \n, and ea hex address 508 | 509 | BYTES_PER_LINE = 16 510 | MAX_BYTES_HEX_DUMP = BYTES_PER_LINE * 64 # 64 lines 511 | 512 | printLen = len(data) 513 | if printLen > MAX_BYTES_HEX_DUMP: 514 | printLen = MAX_BYTES_HEX_DUMP 515 | plg_print(f"Only hexdump first {MAX_BYTES_HEX_DUMP} bytes") 516 | 517 | nLines = printLen // BYTES_PER_LINE # Number of lines 518 | nOdd = printLen % BYTES_PER_LINE # Number of bytes at last line 519 | 520 | isStr = True 521 | sHex = str() 522 | for i in range(printLen): 523 | # Accept NULL char in string 524 | if isStr and (chr(data[i]) not in string.printable) and (data[i] != 0): 525 | isStr = False 526 | 527 | if i % BYTES_PER_LINE == 0: 528 | sHex += f"{idaapi.ea2str(start + i)}: " 529 | 530 | sHex += f"{data[i]:02X} " 531 | 532 | if (i % BYTES_PER_LINE == BYTES_PER_LINE - 1) or (i == printLen - 1): 533 | # add the end of data or end of a line 534 | if nLines: 535 | lineIdx = i // BYTES_PER_LINE # current line number 536 | low = lineIdx * BYTES_PER_LINE 537 | high = i + 1 538 | else: 539 | low = 0 540 | high = printLen 541 | 542 | sHex += " " 543 | 544 | # Padding last line 545 | if i == printLen - 1 and nLines and nOdd: 546 | sHex += " " * (BYTES_PER_LINE - nOdd) * 3 547 | 548 | for j in range(low, high): 549 | ch = chr(data[j]) 550 | sHex += ch if ch.isalnum() else "." 551 | 552 | sHex += "\n" 553 | 554 | # Print out the hexdump string 555 | print(sHex) 556 | 557 | if isStr: 558 | txt = str(data).rstrip(chr(0)) # remove NULL chars at end txt 559 | print(f"String result: '{txt}'") 560 | idaapi.set_cmt(start, f"'{txt}'", 1) 561 | 562 | ret = idaapi.ask_yn(idaapi.ASKBTN_NO, "AUTOHIDE SESSION\nDo you want to patch selected range with result data ?") 563 | if ret != idaapi.ASKBTN_CANCEL: 564 | if ret == idaapi.ASKBTN_YES: 565 | idaapi.patch_bytes(start, bytes(data)) 566 | ret = idaapi.ask_yn(idaapi.ASKBTN_NO, "AUTOHIDE SESSION\nDo you want to dump result data to file ?") 567 | if ret == idaapi.ASKBTN_YES: 568 | dump_data_to_file(f"{idaapi.get_root_filename()}_Dump_At_0x{start:X}_Size_{len(data)}.dump", data) 569 | 570 | 571 | def base64_decode(std): 572 | addr = idc.BADADDR 573 | ea = idc.get_screen_ea() 574 | flags = idaapi.get_flags(ea) 575 | 576 | if idc.is_strlit(flags): 577 | addr = ea # cursor is on the string 578 | elif idc.is_code(flags): 579 | addr = idc.get_first_dref_from(ea) # get data reference from the instruction 580 | 581 | if addr == idc.BADADDR: 582 | plg_print("No string or reference to the string found\n") 583 | return 584 | 585 | b64str_enc = idc.get_strlit_contents(addr, -1, idc.get_str_type(addr)) 586 | if not b64str_enc: 587 | plg_print(f"Could not get string at address 0x{addr:X}") 588 | return 589 | 590 | try: 591 | b64str_dec = base64.standard_b64decode(b64str_enc) if std else base64.urlsafe_b64decode(b64str_enc) 592 | except ValueError as e: 593 | plg_print(f"Could not decode.\n{str(e)}") 594 | return 595 | 596 | if b64str_dec: 597 | plg_print(f"Base64 decode of string '{b64str_enc}':") 598 | process_data_result(ea, bytearray(b64str_dec)) 599 | 600 | 601 | def str_to_bytes(sInput): 602 | """ str -> bytearray """ 603 | try: 604 | s = sInput.strip() # remove trailing white spaces 605 | if s.startswith('"') or s.startswith("'"): 606 | s = s[1:] 607 | if s.endswith('"') or s.endswith("'"): 608 | s = s[:-1] 609 | return bytearray(s.encode("utf-8")) 610 | except ValueError: 611 | return None 612 | 613 | 614 | def hex_to_bytes(sInput): 615 | """ hex str to bytearray """ 616 | s = sInput.lower().replace('0x', '').replace('\\x', '') 617 | s = ''.join("0" + c if len(c) % 2 else c for c in s.split()) # remove all white spaces 618 | try: 619 | s = bytes.fromhex(s) 620 | except ValueError: 621 | plg_print(f"Invalid hex string input '{sInput}'") 622 | s = None 623 | return s 624 | 625 | 626 | def is_str(s): 627 | return s.startswith(('"', "'")) and s.endswith(('"', "'")) 628 | 629 | 630 | def xor_data(data, key): 631 | """ 632 | data: bytes 633 | key: bytes 634 | 635 | return: bytes 636 | """ 637 | output = bytearray(len(data)) 638 | for i, b in enumerate(data): 639 | output[i] = b ^ key[i % len(key)] 640 | return output 641 | 642 | 643 | def nop_hider(): 644 | hides = [] 645 | in_nop_sled = False 646 | curr_pos = 0 647 | sled_len = 0 648 | 649 | for fn_ea in idautils.Functions(): 650 | pfn = idaapi.get_func(fn_ea) 651 | if not pfn: 652 | continue 653 | for ea in range(pfn.start_ea, pfn.end_ea): 654 | b = idaapi.get_byte(ea) 655 | if b in (0x90, 0xCC): 656 | sled_len += 1 657 | if not in_nop_sled: 658 | in_nop_sled = True 659 | curr_pos = ea 660 | else: 661 | if in_nop_sled: 662 | in_nop_sled = False 663 | hides.append([curr_pos, sled_len]) 664 | curr_pos = 0 665 | sled_len = 0 666 | 667 | # at end of function 668 | if in_nop_sled and sled_len > 1: 669 | hides.append([curr_pos, sled_len]) 670 | 671 | if len(hides) == 0: 672 | plg_print("Found nothing NOPs block") 673 | 674 | for h in hides: 675 | if h[1] > 1: 676 | plg_print(f"Hide range: 0x{h[0]:X} - 0x{h[0] + h[1] -1:X}") 677 | idaapi.del_hidden_range(h[0] + h[1] - 1) 678 | idc.add_hidden_range(h[0], h[0] + h[1], '[NOPs]', '', '', idc.DEFCOLOR) 679 | idc.update_hidden_range(h[0], False) 680 | 681 | plg_print(f"Hidding {len(hides)} NOPs block") 682 | 683 | 684 | def turn_off_ida_decision(): 685 | sel, start, end = lazy_read_selection() 686 | if not sel: 687 | return 0 688 | 689 | plg_print(f"Turn off IDA auto analysis for range 0x{start:X} - 0x{end - 1:X}") 690 | idaapi.revert_ida_decisions(start, end) 691 | return 1 692 | 693 | 694 | def lazy_get_bytes(ea, size): 695 | if idaapi.is_debugger_on(): 696 | return idaapi.dbg_read_memory(ea, size) 697 | else: 698 | return idaapi.get_bytes(ea, size) 699 | 700 | 701 | class VulnChoose(idaapi.Choose): 702 | """ 703 | Chooser class to display result of format string vuln scan 704 | """ 705 | def __init__(self, title, items, icon, embedded=False): 706 | idaapi.Choose.__init__(self, title, [["Address", 20], ["Function", 30], ["Format", 30]], embedded=embedded) 707 | self.items = items 708 | self.icon = 45 709 | 710 | def GetItems(self): 711 | return self.items 712 | 713 | def SetItems(self, items): 714 | self.items = [] if items is None else items 715 | 716 | def OnClose(self): 717 | pass 718 | 719 | def OnGetLine(self, n): 720 | return self.items[n] 721 | 722 | def OnGetSize(self): 723 | return len(self.items) 724 | 725 | def OnSelectLine(self, n): 726 | idc.jumpto(int(self.items[n][0], 16)) 727 | 728 | 729 | class hotkey_action_handler_t(idaapi.action_handler_t): 730 | """ 731 | Action handler for hotkey actions 732 | """ 733 | def __init__(self, action): 734 | idaapi.action_handler_t.__init__(self) 735 | self.action = action 736 | 737 | def activate(self, ctx): 738 | if self.action == ACTION_HOTKEY_COPY_EA[0]: 739 | ea = idc.get_screen_ea() 740 | copy_to_clip(f"0x{ea:X}") 741 | plg_print(f"Address '0x{ea:X}' copied to clipboard") 742 | elif self.action == ACTION_HOTKEY_COPY_RVA[0]: 743 | copy_rva() 744 | elif self.action == ACTION_HOTKEY_COPY_FOFS[0]: 745 | copy_file_offset() 746 | elif self.action == ACTION_HOTKEY_COPY_NAME[0]: 747 | copy_highlight_name() 748 | elif self.action == ACTION_HOTKEY_PASTE_NAME[0]: 749 | paste_highlight_name() 750 | elif self.action == ACTION_HOTKEY_GOTO_CLIP[0]: 751 | goto_clip_text() 752 | elif self.action == ACTION_HOTKEY_GOTO_FOFS[0]: 753 | goto_file_ofs() 754 | elif self.action == ACTION_HOTKEY_GOTO_RVA[0]: 755 | goto_rva() 756 | elif self.action == ACTION_HOTKEY_SEARCH_GOOGLE[0]: 757 | search_web(0) 758 | elif self.action == ACTION_HOTKEY_SEARCH_MSDOC[0]: 759 | search_web(1) 760 | elif self.action == ACTION_HOTKEY_SEARCH_BING[0]: 761 | search_web(2) 762 | elif self.action == ACTION_HOTKEY_SEARCH_GITHUB[0]: 763 | search_web(3) 764 | elif self.action == ACTION_HOTKEY_OPEN_URL[0]: 765 | text = get_selected_text() 766 | if text: 767 | idaapi.open_url(text) 768 | else: 769 | return 0 770 | 771 | return 1 772 | 773 | def update(self, ctx): 774 | return idaapi.AST_ENABLE_ALWAYS 775 | 776 | 777 | class menu_action_handler_t(idaapi.action_handler_t): 778 | """ 779 | Action handler for menu actions 780 | """ 781 | def __init__(self, action): 782 | idaapi.action_handler_t.__init__(self) 783 | self.action = action 784 | self.last_hex = "AA BB CC DD" 785 | 786 | def activate(self, ctx): 787 | if self.action in ACTION_MENU_CONVERT: 788 | sel, start, end = lazy_read_selection() 789 | if not sel: 790 | plg_print("Nothing to convert.") 791 | return 0 792 | 793 | size = end - start 794 | data = lazy_get_bytes(start, size) 795 | if isinstance(data, str): # python2 compatibility 796 | data = bytearray(data) 797 | 798 | name = idc.get_name(start, idc.GN_VISIBLE) 799 | if not name: 800 | name = "data" 801 | if data: 802 | output = None 803 | plg_print(f"Dump from 0x{start:X} to 0x{end - 1:X} ({size} bytes):") 804 | if self.action == ACTION_MENU_CONVERT[0]: 805 | # escaped string 806 | output = "".join(f"\\x{b:02X}" for b in data) 807 | 808 | elif self.action == ACTION_MENU_CONVERT[1]: 809 | # hex string, space 810 | output = " ".join(f"{b:02X}" for b in data) 811 | 812 | elif self.action == ACTION_MENU_CONVERT[2]: 813 | # C array 814 | output = f"unsigned char {name}[{size}] = {{" 815 | for i in range(size): 816 | if i % 16 == 0: 817 | output += "\n " 818 | output += f"0x{data[i]:02X}, " 819 | output = output[:-2] + "\n};" 820 | 821 | elif self.action == ACTION_MENU_CONVERT[3]: 822 | # C array word 823 | data += b"\x00" 824 | array_size = (size + 1) // 2 825 | output = f"unsigned short {name}[{array_size}] = {{" 826 | for i in range(0, size, 2): 827 | if i % 16 == 0: 828 | output += "\n " 829 | output += f"0x{u16(data[i:i+2]):04X}, " 830 | output = output[:-2] + "\n};" 831 | 832 | elif self.action == ACTION_MENU_CONVERT[4]: 833 | # C array dword 834 | data += b"\x00" * 3 835 | array_size = (size + 3) // 4 836 | output = f"unsigned int {name}[{array_size}] = {{" 837 | for i in range(0, size, 4): 838 | if i % 32 == 0: 839 | output += "\n " 840 | output += f"0x{u32(data[i:i+4]):08X}, " 841 | output = output[:-2] + "\n};" 842 | 843 | elif self.action == ACTION_MENU_CONVERT[5]: 844 | # C array qword 845 | data += b"\x00" * 7 846 | array_size = (size + 7) // 8 847 | output = f"unsigned __int64 {name}[{array_size}] = {{" 848 | for i in range(0, size, 8): 849 | if i % 32 == 0: 850 | output += "\n " 851 | output += f"0x{u64(data[i:i+8]):016X}, " 852 | output = output[:-2] + "\n};" 853 | 854 | elif self.action == ACTION_MENU_CONVERT[6]: 855 | # python list 856 | output = f"{name} = [{', '.join(f'0x{b:02X}' for b in data)}]" 857 | 858 | elif self.action == ACTION_MENU_CONVERT[7]: 859 | # python list word 860 | data += b"\x00" 861 | output = f"{name} = [{', '.join(f'0x{u16(data[i:i+2]):04X}' for i in range(0, size, 2))}]" 862 | 863 | elif self.action == ACTION_MENU_CONVERT[8]: 864 | # python list dword 865 | data += b"\x00" * 3 866 | output = f"{name} = [{', '.join(f'0x{u32(data[i:i+4]):08X}' for i in range(0, size, 4))}]" 867 | 868 | elif self.action == ACTION_MENU_CONVERT[9]: 869 | # python list qword 870 | data += b"\x00" * 7 871 | output = f"{name} = [{', '.join(f'{u64(data[i:i+8]):016X}' for i in range(0, size, 8))}]" 872 | 873 | elif self.action == ACTION_MENU_CONVERT[10]: 874 | # MASM byte array 875 | header = f"{name} db " 876 | output = header 877 | for i in range(size): 878 | if i and i % 16 == 0: 879 | output += "\n" 880 | output += " " * len(header) 881 | output += f"0{data[i]:02X}h, " 882 | output = output[:-2] 883 | 884 | elif self.action == ACTION_MENU_CONVERT[11]: 885 | # GNU ASM byte array 886 | header = f"{name}: .byte " 887 | output = header 888 | for i in range(size): 889 | if i and i % 16 == 0: 890 | output += "\n" 891 | output += " " * len(header) 892 | output += f"0x{data[i]:02X}, " 893 | output = output[:-2] 894 | 895 | if output: 896 | print(output) 897 | copy_to_clip(output) 898 | output = None 899 | 900 | elif self.action == ACTION_MENU_COPY_DATA: 901 | sel, start, end = lazy_read_selection() 902 | if not sel: 903 | return 0 904 | 905 | data = lazy_get_bytes(start, end - start) 906 | if isinstance(data, str): 907 | data = bytearray(data) 908 | output = "".join(f"{b:02X}" for b in data) 909 | copy_to_clip(output) 910 | plg_print(f"Hex string '{output}' copied") 911 | 912 | elif self.action == ACTION_MENU_COPY_STR: 913 | ea = get_ea_from_highlight() 914 | if idc.is_strlit(idc.get_full_flags(ea)): 915 | s = str(idc.get_strlit_contents(ea, -1, idc.get_str_type(ea))) 916 | if s.startswith("b'") and s.endswith("'"): # byte string 917 | s = s[2:-1] 918 | copy_to_clip(s) 919 | plg_print(f"'{s}' copied") 920 | else: 921 | plg_print("Current EA not in a string") 922 | 923 | elif self.action == ACTION_MENU_DUMP_DATA: 924 | sel, start, end = lazy_read_selection() 925 | if not sel: 926 | return 0 927 | 928 | size = end - start 929 | data = lazy_get_bytes(start, size) 930 | if data: 931 | if len(data) < size: 932 | plg_print("Request {size} bytes, only get {len(data)} bytes") 933 | size = len(data) 934 | dump_data_to_file(f"{idaapi.get_root_filename()}_Dump_At_0x{start:X}_Size_{size}.dump", data) 935 | else: 936 | plg_print(f"0x{start:X}: unable to get {size} bytes") 937 | 938 | elif self.action == ACTION_MENU_DUMP_SEG: 939 | ea = idc.here() 940 | seg = idaapi.getseg(ea) 941 | if not seg: 942 | plg_print(f"0x{ea:X} Unable to get segment at current ea") 943 | return 0 944 | 945 | size = seg.end_ea - seg.start_ea 946 | data = lazy_get_bytes(seg.start_ea, size) 947 | if data: 948 | if len(data) < size: 949 | plg_print("Request {size} bytes, only get {len(data)} bytes") 950 | size = len(data) 951 | dump_data_to_file(f"{idaapi.get_root_filename()}_Dump_Segment_{idaapi.get_segm_name(seg).lstrip('.')}_Size_{size}.dump", data) 952 | else: 953 | plg_print(f"0x{seg.start_ea:X}: unable to get {size} bytes") 954 | 955 | elif self.action == ACTION_MENU_XOR_DATA: 956 | sel, start, end = lazy_read_selection() 957 | if not sel: 958 | return 0 959 | 960 | size = end - start 961 | 962 | key = idaapi.ask_str(self.last_hex, 0, "Xor with hex values (or a string begin and end with\" or ')...") 963 | if not key: 964 | return 0 965 | 966 | bytes_key = bytearray() 967 | if is_str(key): 968 | bytes_key = str_to_bytes(key) 969 | else: 970 | bytes_key = hex_to_bytes(key) 971 | 972 | if not bytes_key: 973 | return 0 974 | 975 | self.last_hex = key # store for later asking 976 | 977 | data = lazy_get_bytes(start, end - start) 978 | if isinstance(data, str): # python2 compatibility 979 | data = bytearray(data) 980 | 981 | output = xor_data(data, bytes_key) 982 | if not output: 983 | plg_print("Sorry, error occurred. My bug :( Please report.") 984 | return 0 985 | 986 | assert size == len(output) 987 | 988 | plg_print(f"Xor result from 0x{start:X} to 0x{end - 1:X} ({end - start} bytes) with {key}:") 989 | process_data_result(start, output) 990 | 991 | elif self.action == ACTION_MENU_FILL_NOP: 992 | sel, start, end = lazy_read_selection() 993 | if not sel: 994 | return 0 995 | 996 | idaapi.patch_bytes(start, b"\x90" * (end - start)) 997 | idc.create_insn(start) 998 | plg_print(f"Fill 0x{start:X} to 0x{end - 1:X} ({end - start} bytes) with NOPs") 999 | 1000 | elif self.action == ACTION_MENU_NOP_HIDER: 1001 | nop_hider() 1002 | 1003 | elif self.action == ACTION_MENU_AUTO_OFF: 1004 | turn_off_ida_decision() 1005 | 1006 | elif self.action == ACTION_MENU_B64STD: 1007 | base64_decode(True) 1008 | 1009 | elif self.action == ACTION_MENU_B64URL: 1010 | base64_decode(False) 1011 | 1012 | elif self.action == ACTION_MENU_SCAN_VUL: 1013 | plg_print("Finding Format String Vulnerability...") 1014 | found = [] 1015 | for addr in idautils.Functions(): 1016 | name = idc.get_func_name(addr) 1017 | if "printf" in name and "v" not in name and idc.get_segm_name(addr) in (".text", ".plt", ".idata"): 1018 | xrefs = idautils.CodeRefsTo(addr, False) 1019 | for xref in xrefs: 1020 | vul = self.check_fmt_function(name, xref) 1021 | if vul: 1022 | found.append(vul) 1023 | if found: 1024 | plg_print(f"Done! {len(found)} possible vulnerabilities found.") 1025 | ch = VulnChoose("Vulnerability", found, None, False) 1026 | ch.Show() 1027 | else: 1028 | plg_print("No format string vulnerabilities found.") 1029 | 1030 | else: 1031 | return 0 1032 | 1033 | return 1 1034 | 1035 | def update(self, ctx): 1036 | return idaapi.AST_ENABLE_FOR_WIDGET if ctx.widget_type in (idaapi.BWN_DISASM, idaapi.BWN_DUMP) \ 1037 | else idaapi.AST_DISABLE_FOR_WIDGET 1038 | 1039 | @staticmethod 1040 | def check_fmt_function(name, addr): 1041 | """ 1042 | Check if the format string argument is not valid 1043 | """ 1044 | function_head = idc.get_func_attr(addr, idc.FUNCATTR_START) 1045 | 1046 | while True: 1047 | addr = idc.prev_head(addr) 1048 | op = idc.print_insn_mnem(addr).lower() 1049 | dst = idc.print_operand(addr, 0) 1050 | 1051 | if op in ("ret", "retn", "jmp", "b") or addr < function_head: 1052 | return None 1053 | 1054 | c = idc.get_cmt(addr, 0) 1055 | if c and c.lower() == "format": 1056 | break 1057 | elif name.endswith(("snprintf_chk",)): 1058 | if op in ("mov", "lea") and dst.endswith(("r8", "r8d", "[esp+10h]")): 1059 | break 1060 | elif name.endswith(("sprintf_chk",)): 1061 | if op in ("mov", "lea") and (dst.endswith(("rcx", "[esp+0Ch]", "R3")) or 1062 | dst.endswith("ecx") and LAZY_BITS == 64): 1063 | break 1064 | elif name.endswith(("snprintf", "fnprintf")): 1065 | if op in ("mov", "lea") and (dst.endswith(("rdx", "[esp+8]", "R2")) or 1066 | dst.endswith("edx") and LAZY_BITS == 64): 1067 | break 1068 | elif name.endswith(("sprintf", "fprintf", "dprintf", "printf_chk")): 1069 | if op in ("mov", "lea") and (dst.endswith(("rsi", "[esp+4]", "R1")) or 1070 | dst.endswith("esi") and LAZY_BITS == 64): 1071 | break 1072 | elif name.endswith("printf"): 1073 | if op in ("mov", "lea") and (dst.endswith(("rdi", "[esp]", "R0")) or 1074 | dst.endswith("edi") and LAZY_BITS == 64): 1075 | break 1076 | 1077 | # format arg found, check its type and value 1078 | # get last oprend 1079 | op_index = idc.generate_disasm_line(addr, 0).count(",") 1080 | op_type = idc.get_operand_type(addr, op_index) 1081 | opnd = idc.print_operand(addr, op_index) 1082 | 1083 | if op_type == idc.o_reg: 1084 | # format is in register, try to track back and get the source 1085 | _addr = addr 1086 | while True: 1087 | _addr = idc.prev_head(_addr) 1088 | _op = idc.print_insn_mnem(_addr).lower() 1089 | if _op in ("ret", "retn", "jmp", "b") or _addr < function_head: 1090 | break 1091 | elif _op in ("mov", "lea", "ldr") and idc.print_operand(_addr, 0) == opnd: 1092 | op_type = idc.get_operand_type(_addr, 1) 1093 | opnd = idc.print_operand(_addr, 1) 1094 | addr = _addr 1095 | break 1096 | 1097 | if op_type in (idc.o_imm, idc.o_mem): 1098 | # format is a memory address, check if it's in writable segment 1099 | op_addr = idc.get_operand_value(addr, op_index) 1100 | seg = idaapi.getseg(op_addr) 1101 | if seg: 1102 | if not seg.perm & idaapi.SEGPERM_WRITE: 1103 | # format is in read-only segment 1104 | return None 1105 | 1106 | plg_print(f"0x{addr:X}: Possible Vulnerability: {name}, format = {opnd}") 1107 | return [f"0x{addr:X}", name, opnd] 1108 | 1109 | 1110 | class hexrays_action_handler_t(idaapi.action_handler_t): 1111 | """ 1112 | Action handler for hexrays actions 1113 | """ 1114 | def __init__(self, action): 1115 | idaapi.action_handler_t.__init__(self) 1116 | self.action = action 1117 | self.ret_type = {} 1118 | 1119 | def activate(self, ctx): 1120 | if self.action == ACTION_HX_REMOVE_RET_TYPE[0]: 1121 | vdui = idaapi.get_widget_vdui(ctx.widget) 1122 | self.remove_rettype(vdui) 1123 | vdui.refresh_ctext() 1124 | else: 1125 | return 0 1126 | 1127 | return 1 1128 | 1129 | def update(self, ctx): 1130 | vdui = idaapi.get_widget_vdui(ctx.widget) 1131 | return idaapi.AST_ENABLE_FOR_WIDGET if vdui else idaapi.AST_DISABLE_FOR_WIDGET 1132 | 1133 | def remove_rettype(self, vu): 1134 | if vu.item.citype == idaapi.VDI_FUNC: 1135 | # current function 1136 | ea = vu.cfunc.entry_ea 1137 | old_func_type = idaapi.tinfo_t() 1138 | if not vu.cfunc.get_func_type(old_func_type): 1139 | return False 1140 | elif vu.item.citype == idaapi.VDI_EXPR and vu.item.e.is_expr() and vu.item.e.type.is_funcptr(): 1141 | # call xxx 1142 | ea = vu.item.get_ea() 1143 | old_func_type = idaapi.tinfo_t() 1144 | 1145 | func = idaapi.get_func(ea) 1146 | if func: 1147 | try: 1148 | cfunc = idaapi.decompile(func) 1149 | except idaapi.DecompilationFailure: 1150 | return False 1151 | 1152 | if not cfunc.get_func_type(old_func_type): 1153 | return False 1154 | else: 1155 | return False 1156 | else: 1157 | return False 1158 | 1159 | fi = idaapi.func_type_data_t() 1160 | if ea != idaapi.BADADDR and old_func_type.get_func_details(fi): 1161 | # Return type is already void 1162 | if fi.rettype.is_decl_void(): 1163 | # Restore ret type 1164 | if ea not in self.ret_type: 1165 | return True 1166 | ret = self.ret_type[ea] 1167 | else: 1168 | # Save ret type and change it to void 1169 | self.ret_type[ea] = fi.rettype 1170 | ret = idaapi.BT_VOID 1171 | 1172 | # Create new function info with new rettype 1173 | fi.rettype = idaapi.tinfo_t(ret) 1174 | 1175 | # Create new function type with function info 1176 | new_func_type = idaapi.tinfo_t() 1177 | new_func_type.create_func(fi) 1178 | 1179 | # Apply new function type 1180 | if idaapi.apply_tinfo(ea, new_func_type, idaapi.TINFO_DEFINITE): 1181 | return vu.refresh_view(True) 1182 | 1183 | return False 1184 | 1185 | 1186 | class UI_Hook(idaapi.UI_Hooks): 1187 | def __init__(self): 1188 | idaapi.UI_Hooks.__init__(self) 1189 | 1190 | def finish_populating_widget_popup(self, widget, popup): 1191 | # attach Searchs menu to all widget 1192 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_SEARCH_GOOGLE[0], PLUGIN_POPUP) 1193 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_SEARCH_MSDOC[0], PLUGIN_POPUP) 1194 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_SEARCH_BING[0], PLUGIN_POPUP) 1195 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_SEARCH_GITHUB[0], PLUGIN_POPUP) 1196 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_OPEN_URL[0], PLUGIN_POPUP) 1197 | idaapi.attach_action_to_popup(widget, popup, None, PLUGIN_POPUP) 1198 | 1199 | widget_type = idaapi.get_widget_type(widget) 1200 | if widget_type in [idaapi.BWN_DISASM, idaapi.BWN_DUMP]: 1201 | for action in ACTION_MENU_CONVERT: 1202 | idaapi.attach_action_to_popup(widget, popup, action, PLUGIN_POPUP + "Convert/") 1203 | 1204 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_COPY_DATA, PLUGIN_POPUP) 1205 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_COPY_STR, PLUGIN_POPUP) 1206 | idaapi.attach_action_to_popup(widget, popup, None, PLUGIN_POPUP) 1207 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_DUMP_DATA, PLUGIN_POPUP) 1208 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_DUMP_SEG, PLUGIN_POPUP) 1209 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_XOR_DATA, PLUGIN_POPUP) 1210 | 1211 | if CAN_NOP: 1212 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_FILL_NOP, PLUGIN_POPUP) 1213 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_NOP_HIDER, PLUGIN_POPUP) 1214 | 1215 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_AUTO_OFF, PLUGIN_POPUP) 1216 | idaapi.attach_action_to_popup(widget, popup, None, PLUGIN_POPUP) 1217 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_B64STD, PLUGIN_POPUP) 1218 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_B64URL, PLUGIN_POPUP) 1219 | idaapi.attach_action_to_popup(widget, popup, None, PLUGIN_POPUP) 1220 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_COPY_EA[0], PLUGIN_POPUP) 1221 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_COPY_RVA[0], PLUGIN_POPUP) 1222 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_COPY_FOFS[0], PLUGIN_POPUP) 1223 | idaapi.attach_action_to_popup(widget, popup, None, PLUGIN_POPUP) 1224 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_COPY_NAME[0], PLUGIN_POPUP) 1225 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_PASTE_NAME[0], PLUGIN_POPUP) 1226 | idaapi.attach_action_to_popup(widget, popup, None, PLUGIN_POPUP) 1227 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_GOTO_CLIP[0], PLUGIN_POPUP) 1228 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_GOTO_FOFS[0], PLUGIN_POPUP) 1229 | idaapi.attach_action_to_popup(widget, popup, ACTION_HOTKEY_GOTO_RVA[0], PLUGIN_POPUP) 1230 | 1231 | if widget_type == idaapi.BWN_DISASM and (LAZY_ARCH, LAZY_BITS) in [(idaapi.PLFM_386, 32), 1232 | (idaapi.PLFM_386, 64), 1233 | (idaapi.PLFM_ARM, 32), ]: 1234 | idaapi.attach_action_to_popup(widget, popup, None, PLUGIN_POPUP) 1235 | idaapi.attach_action_to_popup(widget, popup, ACTION_MENU_SCAN_VUL, PLUGIN_POPUP) 1236 | 1237 | 1238 | class HexRays_Hook: 1239 | def callback(self, event, *args): 1240 | if event == idaapi.hxe_populating_popup: 1241 | if not ENABLE_REMOVE_RETTYPE: 1242 | return 0 1243 | 1244 | form, phandle, vu = args 1245 | if vu.item.citype == idaapi.VDI_FUNC or (vu.item.citype == idaapi.VDI_EXPR and vu.item.e.is_expr() and vu.item.e.type.is_funcptr()): 1246 | idaapi.attach_action_to_popup(form, phandle, ACTION_HX_REMOVE_RET_TYPE[0], PLUGIN_POPUP) 1247 | elif event == idaapi.hxe_double_click: 1248 | vu, _shift_state = args 1249 | # auto jump to target if clicked item is xxx->func(); 1250 | if vu.item.citype == idaapi.VDI_EXPR and vu.item.e.is_expr(): 1251 | expr = idaapi.tag_remove(vu.item.e.print1(None)) 1252 | if "->" in expr: 1253 | # find target function 1254 | name = expr.split("->")[-1] 1255 | addr = idc.get_name_ea_simple(name) 1256 | if addr == idaapi.BADADDR: 1257 | # try class::function 1258 | e = vu.item.e 1259 | while e.x: 1260 | e = e.x 1261 | addr = idc.get_name_ea_simple(f"{str(e.type).split()[0]}::{name}") 1262 | 1263 | if addr != idaapi.BADADDR: 1264 | idc.jumpto(addr) 1265 | return 1 1266 | return 0 1267 | 1268 | 1269 | class LazyIDA_t(idaapi.plugin_t): 1270 | flags = idaapi.PLUGIN_HIDE # | idaapi.PLUGIN_UNL for fix bugs, debug 1271 | comment = PLUGIN_NAME 1272 | help = "" 1273 | wanted_name = PLUGIN_NAME 1274 | wanted_hotkey = "" 1275 | 1276 | def __init__(self): 1277 | self.ui_hook = None 1278 | self.hx_hook = None 1279 | self.hexrays_inited = False 1280 | self.registered_hotkey_actions = [] 1281 | self.registered_menu_actions = [] 1282 | self.registered_hexray_actions = [] 1283 | 1284 | def init(self): 1285 | plg_print(f"v{PLUGIN_VERSION} - plugin has been loaded.") 1286 | 1287 | # Register hotkey actions 1288 | for HK_ACT in ALL_HOTKEY_ACTIONS: 1289 | action = idaapi.action_desc_t(HK_ACT[0], # name 1290 | HK_ACT[1], # label 1291 | hotkey_action_handler_t(HK_ACT[0]), # action handler 1292 | HK_ACT[2], # shortcut 1293 | HK_ACT[3], # tooltip 1294 | HK_ACT[4]) # iconid 1295 | idaapi.register_action(action) 1296 | self.registered_hotkey_actions.append(action.name) 1297 | 1298 | # Register menu actions 1299 | menu_actions = ( 1300 | idaapi.action_desc_t(ACTION_MENU_CONVERT[0], "Convert to escaped hex string", menu_action_handler_t(ACTION_MENU_CONVERT[0]), None, None, 80), 1301 | idaapi.action_desc_t(ACTION_MENU_CONVERT[1], "Convert to space hex string", menu_action_handler_t(ACTION_MENU_CONVERT[1]), None, None, 8), 1302 | idaapi.action_desc_t(ACTION_MENU_CONVERT[2], "Convert to C/C++ array (BYTE)", menu_action_handler_t(ACTION_MENU_CONVERT[2]), None, None, 38), 1303 | idaapi.action_desc_t(ACTION_MENU_CONVERT[3], "Convert to C/C++ array (WORD)", menu_action_handler_t(ACTION_MENU_CONVERT[3]), None, None, 38), 1304 | idaapi.action_desc_t(ACTION_MENU_CONVERT[4], "Convert to C/C++ array (DWORD)", menu_action_handler_t(ACTION_MENU_CONVERT[4]), None, None, 38), 1305 | idaapi.action_desc_t(ACTION_MENU_CONVERT[5], "Convert to C/C++ array (QWORD)", menu_action_handler_t(ACTION_MENU_CONVERT[5]), None, None, 38), 1306 | idaapi.action_desc_t(ACTION_MENU_CONVERT[6], "Convert to python list (BYTE)", menu_action_handler_t(ACTION_MENU_CONVERT[6]), None, None, 201), 1307 | idaapi.action_desc_t(ACTION_MENU_CONVERT[7], "Convert to python list (WORD)", menu_action_handler_t(ACTION_MENU_CONVERT[7]), None, None, 201), 1308 | idaapi.action_desc_t(ACTION_MENU_CONVERT[8], "Convert to python list (DWORD)", menu_action_handler_t(ACTION_MENU_CONVERT[8]), None, None, 201), 1309 | idaapi.action_desc_t(ACTION_MENU_CONVERT[9], "Convert to python list (QWORD)", menu_action_handler_t(ACTION_MENU_CONVERT[9]), None, None, 201), 1310 | idaapi.action_desc_t(ACTION_MENU_CONVERT[10], "Convert to MASM array (BYTE)", menu_action_handler_t(ACTION_MENU_CONVERT[10]), None, None, 38), 1311 | idaapi.action_desc_t(ACTION_MENU_CONVERT[11], "Convert to GNU ASM array (BYTE)", menu_action_handler_t(ACTION_MENU_CONVERT[11]), None, None, 38), 1312 | idaapi.action_desc_t(ACTION_MENU_COPY_DATA, "Copy hex data", menu_action_handler_t(ACTION_MENU_COPY_DATA), None, None, 0x1F), 1313 | idaapi.action_desc_t(ACTION_MENU_COPY_STR, "Copy string", menu_action_handler_t(ACTION_MENU_COPY_STR), None, None, 0x1F), 1314 | idaapi.action_desc_t(ACTION_MENU_DUMP_DATA, "Dump selected data to file", menu_action_handler_t(ACTION_MENU_DUMP_DATA), None, None, 0x1B), 1315 | idaapi.action_desc_t(ACTION_MENU_DUMP_SEG, "Dump current segment to file", menu_action_handler_t(ACTION_MENU_DUMP_SEG), None, None, 0x1B), 1316 | idaapi.action_desc_t(ACTION_MENU_XOR_DATA, "Get xored data", menu_action_handler_t(ACTION_MENU_XOR_DATA), None, None, 9), 1317 | idaapi.action_desc_t(ACTION_MENU_AUTO_OFF, "Revert IDA Decision", menu_action_handler_t(ACTION_MENU_AUTO_OFF), None, None, 9), 1318 | idaapi.action_desc_t(ACTION_MENU_B64STD, "Base64Std decode", menu_action_handler_t(ACTION_MENU_B64STD), None, None, 9), 1319 | idaapi.action_desc_t(ACTION_MENU_B64URL, "Base64Url decode", menu_action_handler_t(ACTION_MENU_B64URL), None, None, 9), 1320 | idaapi.action_desc_t(ACTION_MENU_SCAN_VUL, "Scan format string vulnerabilities", menu_action_handler_t(ACTION_MENU_SCAN_VUL), None, None, 160), 1321 | ) 1322 | 1323 | if CAN_NOP: 1324 | menu_actions += (idaapi.action_desc_t(ACTION_MENU_FILL_NOP, "Fill with NOPs", menu_action_handler_t(ACTION_MENU_FILL_NOP), None, None, 9), 1325 | idaapi.action_desc_t(ACTION_MENU_NOP_HIDER, "NOPs Hider", menu_action_handler_t(ACTION_MENU_NOP_HIDER), None, None, 9)) 1326 | 1327 | for action in menu_actions: 1328 | idaapi.register_action(action) 1329 | self.registered_menu_actions.append(action.name) 1330 | 1331 | # Add ui hook 1332 | self.ui_hook = UI_Hook() 1333 | self.ui_hook.hook() 1334 | 1335 | # Add hexrays ui callback 1336 | if idaapi.init_hexrays_plugin(): 1337 | hx_actions = ( 1338 | idaapi.action_desc_t(ACTION_HX_REMOVE_RET_TYPE[0], 1339 | ACTION_HX_REMOVE_RET_TYPE[1], 1340 | hexrays_action_handler_t(ACTION_HX_REMOVE_RET_TYPE[0]), 1341 | ACTION_HX_REMOVE_RET_TYPE[2], 1342 | ACTION_HX_REMOVE_RET_TYPE[3], 1343 | -1), 1344 | ) 1345 | for action in hx_actions: 1346 | idaapi.register_action(action) 1347 | self.registered_hexray_actions.append(action.name) 1348 | 1349 | self.hx_hook = HexRays_Hook() 1350 | idaapi.install_hexrays_callback(self.hx_hook.callback) 1351 | self.hexrays_inited = True 1352 | 1353 | addon = idaapi.addon_info_t() 1354 | addon.id = "htc_lazyida" 1355 | addon.name = PLUGIN_NAME 1356 | addon.producer = "HTC (Original: Lays - tw.l4ys.lazyida)" 1357 | addon.url = "https://github.com/HongThatCong/LazyIDA" 1358 | addon.version = PLUGIN_VERSION 1359 | idaapi.register_addon(addon) 1360 | 1361 | return idaapi.PLUGIN_KEEP 1362 | 1363 | def run(self, arg): 1364 | pass 1365 | 1366 | def term(self): 1367 | if self.ui_hook: 1368 | self.ui_hook.unhook() 1369 | self.ui_hook = None 1370 | 1371 | # Unregister actions 1372 | if self.registered_hotkey_actions: 1373 | for action in self.registered_hotkey_actions: 1374 | idaapi.unregister_action(action) 1375 | del self.registered_hotkey_actions[:] 1376 | 1377 | if self.registered_menu_actions: 1378 | for action in self.registered_menu_actions: 1379 | idaapi.unregister_action(action) 1380 | del self.registered_menu_actions[:] 1381 | 1382 | if self.hexrays_inited: 1383 | if self.hx_hook: 1384 | idaapi.remove_hexrays_callback(self.hx_hook.callback) 1385 | self.hx_hook = None 1386 | 1387 | # Unregister hexrays actions 1388 | if self.registered_hexray_actions: 1389 | for action in self.registered_hexray_actions: 1390 | idaapi.unregister_action(action) 1391 | del self.registered_hexray_actions[:] 1392 | 1393 | idaapi.term_hexrays_plugin() 1394 | 1395 | plg_print("plugin terminated") 1396 | 1397 | 1398 | def PLUGIN_ENTRY(): 1399 | return LazyIDA_t() 1400 | --------------------------------------------------------------------------------