├── LICENSE ├── README.md ├── __init__.py ├── plugin.json └── readme_pics ├── code_after.png ├── code_before.png └── menu.png /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CleanTricks 2 | Binary Ninja plugin to clean up some common obfuscation techniques (dirty tricks). 3 | There is an empty template included as well to be able to easily add your own, because there are many many obfuscations out there. However, often they get reused or slightly modified, and then it is useful to have a starting point/example. 4 | Pull requests are very welcome. 5 | 6 | More details available at: 7 | 8 | https://www.janbeck.com/cybersecurity-challenges-ctfs-and-more/cleantricks-to-deal-with-dirty-tricks-binary-ninja-deobfuscation-plugin 9 | 10 | But as a simple example of what this is for, consider the following code: 11 | 12 | ![Code Before](readme_pics/code_before.png) 13 | 14 | The overlapping code is hidden in the literal value, `0x5ebcbff49c3ff49`. It simply increases `r11`, then decreases it again, then jumps to the address right behind the `JZ` check. I have come across a binary that uses this same trick on different registers thousands of times. This plugin allows automating this patch: 15 | 16 | ![Menu](readme_pics/menu.png) 17 | 18 | And then, after the patch, the code is much simpler. In many cases this allows binary ninja to reanalyze the program flow into a much more convienient form. 19 | 20 | ![Menu](readme_pics/code_after.png) 21 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from binaryninja import * 2 | from operator import itemgetter 3 | import time 4 | from collections import namedtuple 5 | 6 | Instruction = namedtuple("Instruction", ["tokens", "address"]) 7 | 8 | KNOWN_OPER = ["xor", "add", "sub", "or", "and"] 9 | 10 | def instr(tup): 11 | tokens, addr = tup 12 | # TODO Perhaps instead of doing this we can leave them as 13 | # InstructionTextToken objects and just utilize the API better?... 14 | tokens = [x.text.lstrip().rstrip() for x in tokens] 15 | tokens = [x for x in tokens if x != ""] 16 | return Instruction(tokens=tokens, address=addr) 17 | 18 | 19 | def sorted_instructions(binview): 20 | """ 21 | Return a sorted list of the instructions in the current viewport. 22 | """ 23 | addrs = [] 24 | instructions = [] 25 | for ii in binview.instructions: 26 | if ii[1] not in addrs: 27 | instructions.append(instr(ii)) 28 | addrs.append(ii[1]) 29 | 30 | del addrs 31 | instructions.sort(key=lambda x: x.address) 32 | return instructions 33 | 34 | 35 | def sliding_window(iterable, window_size): 36 | iterable = iter(iterable) 37 | window = None 38 | try: 39 | window = [next(iterable) for _ in range(window_size)] 40 | except StopIteration as err: 41 | raise ValueError("Window is smaller than generator size") 42 | 43 | while True: 44 | try: 45 | yield window 46 | window = window[1:] + [next(iterable)] 47 | except StopIteration: 48 | break 49 | 50 | 51 | def oper(string, arg1, arg2): 52 | if string == "xor": 53 | return arg1 ^ arg2 54 | if string == "add": 55 | return arg1 + arg2 56 | if string == "sub": 57 | return arg1 - arg2 58 | if string == "or": 59 | return arg1 | arg2 60 | if string == "and": 61 | return arg1 & arg2 62 | 63 | 64 | def clean_tricks_oper_before_jns(bv): 65 | # Pattern we are trying to get rid of: 66 | # 67 | # mov eax, 0xea3e6566 68 | # xor eax, 0x5f69faeb {0xb5579f8d} 69 | # jns 0x4007b6 {0x0} Will never branch! 70 | 71 | # NOTE: How this works: the MOV and XOR instructions are arranged so that 72 | # the operands produce a signed number (MSB is 1). Thus the branch is 73 | # never taken (SF=0). 74 | 75 | start = time.time() 76 | instructions = sorted_instructions(bv) 77 | 78 | for triplet in sliding_window(instructions, 3): 79 | matches = ( 80 | "jns" in triplet[2].tokens and 81 | triplet[1].tokens[0] in KNOWN_OPER and 82 | "mov" in triplet[0].tokens and 83 | 84 | # Registers must match and be a 32bit register 85 | triplet[0].tokens[1] == triplet[1].tokens[1] and triplet[0].tokens[1].startswith("e") 86 | ) 87 | 88 | if not matches: 89 | continue 90 | 91 | value = oper( 92 | triplet[1].tokens[0], 93 | int(triplet[1].tokens[3], 16), 94 | int(triplet[0].tokens[3], 16) 95 | ) 96 | never_branch = value & 0x800000000 != 0 97 | if never_branch: 98 | bv.never_branch(triplet[2].address) 99 | else: 100 | bv.never_branch(triplet[2].address) 101 | 102 | msg = "[CleanTricks] Cutting out useless branch at 0x{0.address:x}".format(triplet[2]) 103 | binaryninja.log_info(msg) 104 | 105 | msg = "[CleanTricks::OperBeforeJNS] Done! Finished in {} seconds.".format(time.time() - start) 106 | binaryninja.log_info(msg) 107 | 108 | 109 | def clean_tricks_oper_before_jne(bv): 110 | # Pattern we are trying to get rid of: 111 | # 112 | # mov eax, 0x2e4ef210 113 | # add eax, 0xd1b10df0 {0x0} 114 | # jne 0x40087e {0x0} Will always branch! 115 | 116 | # NOTE: How this works: the MOV and ADD instructions are arranged perfectly 117 | # so that the result of the add is 0x100000000. Since this is larger than 118 | # the max 32-bit number, EAX is set to 0, ZF=0 and CF=1. JNE jumps if ZF=0 119 | # so the branch is always taken. In the opposite case, if the two numbers 120 | # add to anything not zero, ZF=1 and the JNE is never taken. 121 | 122 | start = time.time() 123 | instructions = sorted_instructions(bv) 124 | 125 | for triplet in sliding_window(instructions, 3): 126 | matches = ( 127 | "jne" in triplet[2].tokens and 128 | triplet[1].tokens[0] in KNOWN_OPER and 129 | "mov" in triplet[0].tokens and 130 | 131 | # Registers must match and be 32bit 132 | triplet[0].tokens[1] == triplet[1].tokens[1] and triplet[1].tokens[1].startswith("e") 133 | ) 134 | 135 | if not matches: 136 | continue 137 | 138 | value = oper( 139 | triplet[1].tokens[0], 140 | int(triplet[1].tokens[3], 16), 141 | int(triplet[0].tokens[3], 16) 142 | ) 143 | always_branch = (value & 0xFFFFFFFF == 0) 144 | if always_branch: 145 | bv.always_branch(triplet[2].address) 146 | else: 147 | bv.never_branch(triplet[2].address) 148 | 149 | msg = "[CleanTricks] Cutting out useless branch at 0x{0.address:x}".format(triplet[2]) 150 | binaryninja.log_info(msg) 151 | 152 | msg = "[CleanTricks::OperBeforeJNE] Done! Finished in {} seconds.".format(time.time() - start) 153 | binaryninja.log_info(msg) 154 | 155 | 156 | def clean_tricks_template(bv): 157 | # empty template to play with 158 | start = time.time() 159 | instructionList = sorted(bv.instructions, key=itemgetter(1)) # sort instructions by address. BN does not provide this in order. Also, there are duplicates... 160 | # or instructionList = bv.instructions , because sometimes listing by block is more useful 161 | last_instruction = None # keep track of the instructions before the one under current consideration 162 | last_instruction2 = None # keep track of the instructions before the one under current consideration 163 | for instruction in instructionList: 164 | # add processing here 165 | last_instruction2 = last_instruction # keep track of the instructions before the one under current consideration 166 | last_instruction = instruction # keep track of the instructions before the one under current consideration 167 | end = time.time() 168 | binaryninja.log_info(repr(end-start)) 169 | binaryninja.log_info("Done. Finished in {} seconds.".format(end-start)) 170 | 171 | 172 | def clean_tricks_push_xor_je_pop(bv): 173 | #push REG, xor REG,REG, je, pop REG 174 | start = time.time() 175 | patchCount = 0 176 | instructionList = sorted(bv.instructions, key=itemgetter(1)) # sort instructions by address. BN does not provide this in order 177 | last_instruction = None 178 | last_instruction2 = None 179 | last_instruction3 = None 180 | last_instruction4 = None 181 | for instruction in instructionList: 182 | if last_instruction2 is not None: 183 | if repr(instruction) == repr(last_instruction): # ignore repeated/duplicate code blocks 184 | continue 185 | if repr(instruction) == repr(last_instruction2): # ignore repeated/duplicate code blocks 186 | continue 187 | if "'push'" in repr(last_instruction2): 188 | if "'xor'" in repr(last_instruction): 189 | if "je" in repr(instruction): 190 | if repr(last_instruction[0][2]) == repr(last_instruction[0][4]): # xor has to happen on single register 191 | if repr(last_instruction[0][2]) == repr(last_instruction2[0][2]): # push xor registers have to match 192 | target_address = int(repr(instruction[0][2]).strip("'"),16) # the target of the je instruction 193 | target_instruction = bv.get_disassembly(target_address) 194 | if "pop" in target_instruction: # has to be a pop instruction 195 | if repr(last_instruction2[0][2]).strip("'") in target_instruction: # push and pop registers have to match 196 | binaryninja.log_info(repr(last_instruction2)) 197 | binaryninja.log_info(repr(last_instruction)) 198 | binaryninja.log_info(repr(instruction)) 199 | binaryninja.log_info(repr(bv.get_disassembly(target_address))) 200 | patchCount = patchCount + 1 201 | bv.convert_to_nop(last_instruction2[1]) 202 | bv.convert_to_nop(last_instruction[1]) 203 | bv.always_branch(instruction[1]) # jump always 204 | bv.convert_to_nop(target_address) 205 | last_instruction4 = last_instruction3 206 | last_instruction3 = last_instruction2 207 | last_instruction2 = last_instruction 208 | last_instruction = instruction 209 | end = time.time() 210 | binaryninja.log_info(repr(end-start)) 211 | binaryninja.log_info("Done. Finished in {} seconds.".format(end-start)) 212 | binaryninja.log_info("{} patches performed.".format(patchCount)) 213 | 214 | 215 | def clean_tricks_jmp_inc_dec(bv): 216 | # jmp (overlaps inc instruction) -> inc REG -> dec REG 217 | start = time.time() 218 | patchCount = 0 219 | instructionList = sorted(bv.instructions, key=itemgetter(1)) # sort instructions by address. BN does not provide this in order 220 | last_instruction = None 221 | last_instruction2 = None 222 | for instruction in instructionList: 223 | if last_instruction2 is not None: 224 | if repr(instruction[0]) == repr(last_instruction[0]): # ignore repeated/duplicate code blocks 225 | continue 226 | if repr(instruction[0]) == repr(last_instruction2[0]): # ignore repeated/duplicate code blocks 227 | continue 228 | if "'jmp'" in repr(last_instruction2): 229 | if "inc" in repr(last_instruction): 230 | if "dec" in repr(instruction): 231 | jmp_address = last_instruction2[1] # the address of the jmp instruction itself 232 | target_address = int(repr(last_instruction2[0][2]).strip("'"),16) # the target of the jmp instruction 233 | # if we jump right back into the mov instruction 234 | if target_address-jmp_address == 1 : 235 | bv.write(jmp_address, b"\x90\x90\x90\x90\x90") 236 | patchCount = patchCount + 1 237 | binaryninja.log_info(repr(last_instruction2)) 238 | binaryninja.log_info(repr(last_instruction)) 239 | binaryninja.log_info(repr(instruction)) 240 | binaryninja.log_info(repr(target_address-jmp_address)) 241 | last_instruction2 = last_instruction 242 | last_instruction = instruction 243 | end = time.time() 244 | binaryninja.log_info("Done. Finished in {} seconds.".format(end-start)) 245 | binaryninja.log_info("{} patches performed.".format(patchCount)) 246 | 247 | 248 | def clean_tricks_mov_xor_je(bv): 249 | # mov REG, CODE -> xor REG,REG -> je CODE 250 | # careful! xor REG,REG may be legitimate... 251 | start = time.time() 252 | patchCount = 0 253 | instructionList = bv.instructions 254 | last_instruction = None 255 | last_instruction2 = None 256 | for instruction in instructionList: 257 | if last_instruction2 is not None: 258 | if "'mov'" in repr(last_instruction2): 259 | if "xor" in repr(last_instruction): 260 | if repr(last_instruction[0][2]) == repr(last_instruction[0][4]): # xor has to happen on single register 261 | if repr(last_instruction2[0][2]) == repr(last_instruction[0][2]): # mov and xor has to happen on identical register 262 | if "je" in repr(instruction): 263 | mov_address = last_instruction2[1] # the address of the mov instruction itself 264 | target_address = int(repr(instruction[0][2]).strip("'"),16) # the target of the je instruction 265 | if target_address-mov_address == 2 : # if we jump right back into the mov instruction 266 | bv.write(mov_address, b"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90") # NOP away obfuscating content 267 | patchCount = patchCount + 1 268 | binaryninja.log_info(repr(last_instruction2)) 269 | binaryninja.log_info(repr(last_instruction)) 270 | binaryninja.log_info(repr(instruction)) 271 | last_instruction2 = last_instruction 272 | last_instruction = instruction 273 | end = time.time() 274 | binaryninja.log_info(repr(end-start)) 275 | binaryninja.log_info("Done. Finished in {} seconds.".format(end-start)) 276 | binaryninja.log_info("{} patches performed.".format(patchCount)) 277 | 278 | 279 | def clean_tricks_mov_xor_je_sorted(bv): 280 | # mov REG, CODE -> xor REG,REG -> je CODE 281 | # careful! xor REG,REG may be legitimate... 282 | start = time.time() 283 | patchCount = 0 284 | instructionList = sorted(bv.instructions, key=itemgetter(1)) # sort instructions by address. BN does not provide this in order 285 | last_instruction = None 286 | last_instruction2 = None 287 | last_instruction3 = None 288 | last_instruction4 = None 289 | last_instruction5 = None 290 | for instruction in instructionList: 291 | if last_instruction5 is not None: 292 | if repr(instruction) in [repr(last_instruction),repr(last_instruction2),repr(last_instruction3),repr(last_instruction4),repr(last_instruction5)]: # ignore repeated/duplicate code blocks 293 | continue 294 | if "'mov'" in repr(last_instruction5): 295 | if last_instruction5[1] == 0x004d52c6 : 296 | binaryninja.log_info(repr(last_instruction5)) 297 | binaryninja.log_info(repr(last_instruction4)) 298 | binaryninja.log_info(repr(last_instruction3)) 299 | binaryninja.log_info(repr(last_instruction2)) 300 | binaryninja.log_info(repr(last_instruction)) 301 | binaryninja.log_info(repr(instruction)) 302 | if "'inc'" in repr(last_instruction4): 303 | if "'dec'" in repr(last_instruction3): 304 | if "jmp" in repr(last_instruction2): 305 | if "xor" in repr(last_instruction): 306 | if "je" in repr(instruction): 307 | if repr(last_instruction[0][2]) == repr(last_instruction[0][4]): # xor has to happen on identical register 308 | if repr(last_instruction[0][2]) == repr(last_instruction5[0][2]): # mov and xor has to happen on identical register 309 | mov_address = last_instruction5[1] # the address of the mov instruction itself 310 | target_address = int(repr(instruction[0][2]).strip("'"),16) # the target of the je instruction 311 | if target_address-mov_address == 2 : # if we jump right back into the mov instruction 312 | bv.write(mov_address, b"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90") # NOP away obfuscating content 313 | patchCount = patchCount + 1 314 | binaryninja.log_info(repr(last_instruction5)) 315 | binaryninja.log_info(repr(last_instruction4)) 316 | binaryninja.log_info(repr(last_instruction3)) 317 | binaryninja.log_info(repr(last_instruction2)) 318 | binaryninja.log_info(repr(last_instruction)) 319 | binaryninja.log_info(repr(instruction)) 320 | last_instruction5 = last_instruction4 321 | last_instruction4 = last_instruction3 322 | last_instruction3 = last_instruction2 323 | last_instruction2 = last_instruction 324 | last_instruction = instruction 325 | end = time.time() 326 | binaryninja.log_info(repr(end-start)) 327 | binaryninja.log_info("Done. Finished in {} seconds.".format(end-start)) 328 | binaryninja.log_info("{} patches performed.".format(patchCount)) 329 | 330 | 331 | def clean_tricks_inc_dec(bv): 332 | # jmp (overlaps inc instruction) -> inc REG -> dec REG 333 | start = time.time() 334 | patchCount = 0 335 | instructionList = sorted(bv.instructions, key=itemgetter(1)) # sort instructions by address. BN does not provide this in order 336 | last_instruction = None 337 | for instruction in instructionList: 338 | if last_instruction is not None: 339 | if repr(instruction[0]) == repr(last_instruction[0]): # ignore repeated/duplicate code blocks 340 | continue 341 | if "inc" in repr(last_instruction): 342 | if "dec" in repr(instruction): 343 | if repr(last_instruction[0][2]) == repr(instruction[0][2]): # inc/dec has to happen on identical register 344 | bv.write(last_instruction[1], b"\x90\x90\x90\x90\x90\x90") 345 | patchCount = patchCount + 1 346 | binaryninja.log_info(repr(last_instruction)) 347 | binaryninja.log_info(repr(instruction)) 348 | last_instruction = instruction 349 | end = time.time() 350 | binaryninja.log_info("Done. Finished in {} seconds.".format(end-start)) 351 | binaryninja.log_info("{} patches performed.".format(patchCount)) 352 | 353 | 354 | def clean_tricks_xor_je(bv): 355 | # xor REG,REG -> je ADDR 356 | start = time.time() 357 | patchCount = 0 358 | instructionList = sorted(bv.instructions, key=itemgetter(1)) # sort instructions by address. BN does not provide this in order 359 | last_instruction = None 360 | last_instruction2 = None 361 | for instruction in instructionList: 362 | if last_instruction is not None: 363 | if repr(instruction[0]) == repr(last_instruction[0]): # ignore repeated/duplicate code blocks 364 | continue 365 | if "xor" in repr(last_instruction): 366 | if "je" in repr(instruction): 367 | if repr(last_instruction[0][2]) == repr(last_instruction[0][4]): # xor has to happen on single register 368 | bv.always_branch(instruction[1]) 369 | start_addr = bv.get_previous_function_start_before(last_instruction[1]) 370 | func = bv.get_function_at(start_addr) 371 | patchCount = patchCount + 1 372 | if func is not None: 373 | func.set_user_instr_highlight(last_instruction[1], HighlightStandardColor.BlueHighlightColor) 374 | start_addr = bv.get_previous_function_start_before(instruction[1]) 375 | func = bv.get_function_at(start_addr) 376 | if func is not None: 377 | func.set_user_instr_highlight(instruction[1], HighlightStandardColor.BlueHighlightColor) 378 | binaryninja.log_info(repr(last_instruction)) 379 | binaryninja.log_info(repr(instruction)) 380 | last_instruction = instruction 381 | end = time.time() 382 | binaryninja.log_info(repr(end-start)) 383 | binaryninja.log_info("Done. Finished in {} seconds.".format(end-start)) 384 | binaryninja.log_info("{} patches performed.".format(patchCount)) 385 | 386 | 387 | def clean_tricks_je_jne(bv): 388 | # je ADDR -> jne ADDR 389 | start = time.time() 390 | patchCount = 0 391 | instructionList = sorted(bv.instructions, key=itemgetter(1)) # sort instructions by address. BN does not provide this in order 392 | last_instruction = None 393 | last_instruction2 = None 394 | for instruction in instructionList: 395 | if last_instruction2 is not None: 396 | if repr(instruction) == repr(last_instruction): # ignore repeated/duplicate code blocks 397 | continue 398 | if repr(instruction) == repr(last_instruction2): # ignore repeated/duplicate code blocks 399 | continue 400 | if "je" in repr(last_instruction): 401 | if "jne" in repr(instruction): 402 | address1 = int(repr(last_instruction[0][2]).strip("'"),16) # the target of the je instruction 403 | address2 = int(repr(instruction[0][2]).strip("'"),16) # the target of the jne instruction 404 | if address2 > (instruction[1] + 1): # make sure target is at least two bytes further than the jne instruction itself 405 | if address1 == address2: 406 | patchCount = patchCount + 1 407 | bv.always_branch(last_instruction[1]) 408 | start_addr = bv.get_previous_function_start_before(last_instruction[1]) 409 | func = bv.get_function_at(start_addr) 410 | if func is not None: 411 | func.set_user_instr_highlight(last_instruction[1], HighlightStandardColor.BlueHighlightColor) 412 | start_addr = bv.get_previous_function_start_before(instruction[1]) 413 | func = bv.get_function_at(start_addr) 414 | if func is not None: 415 | func.set_user_instr_highlight(instruction[1], HighlightStandardColor.BlueHighlightColor) 416 | binaryninja.log_info(repr(last_instruction2)) 417 | binaryninja.log_info(repr(last_instruction)) 418 | binaryninja.log_info(repr(instruction)) 419 | last_instruction2 = last_instruction 420 | last_instruction = instruction 421 | end = time.time() 422 | binaryninja.log_info(repr(end-start)) 423 | binaryninja.log_info("Done. Finished in {} seconds.".format(end-start)) 424 | binaryninja.log_info("{} patches performed.".format(patchCount)) 425 | 426 | 427 | def clean_tricks_all(bv): 428 | clean_tricks_je_jne(bv) 429 | clean_tricks_push_xor_je_pop(bv) 430 | clean_tricks_jmp_inc_dec(bv) 431 | clean_tricks_mov_xor_je(bv) 432 | clean_tricks_mov_xor_je_sorted(bv) 433 | clean_tricks_inc_dec(bv) 434 | clean_tricks_xor_je(bv) 435 | clean_tricks_oper_before_jne(bv) 436 | clean_tricks_oper_before_jns(bv) 437 | 438 | 439 | PluginCommand.register( 440 | "Clean Tricks\\00 - Empty template", 441 | "Empty template to experiment with", 442 | clean_tricks_template 443 | ) 444 | PluginCommand.register( 445 | "Clean Tricks\\01 - Patch 'je ADDR-> jne ADDR' (forward)", 446 | "Patches all 'je ADDR -> jne ADDR' to jmp", 447 | clean_tricks_je_jne 448 | ) 449 | PluginCommand.register( 450 | "Clean Tricks\\02 - Patch 'push REG, xor REG,REG, je, pop REG'", 451 | "Patches all 'push REG, xor REG,REG, je, pop REG' to nop", 452 | clean_tricks_push_xor_je_pop 453 | ) 454 | PluginCommand.register( 455 | "Clean Tricks\\03 - Patch 'jmp -> inc REG -> dec REG' (jmp,inc overlap)", 456 | "Patches all 'jmp - > inc REG -> dec REG' to nops", 457 | clean_tricks_jmp_inc_dec 458 | ) 459 | PluginCommand.register( 460 | "Clean Tricks\\04 - Patch 'mov REG -> xor REG,REG -> je'", 461 | "Patches all 'mov REG -> xor REG,REG -> je' to 'nop'" , 462 | clean_tricks_mov_xor_je 463 | ) 464 | PluginCommand.register( 465 | "Clean Tricks\\05 - Patch 'mov REG -> xor REG,REG -> je' (sort instructions)", 466 | "Patches all 'mov REG -> xor REG,REG -> je' to 'nop' using a sorted list for the instructions", 467 | clean_tricks_mov_xor_je_sorted 468 | ) 469 | PluginCommand.register( 470 | "Clean Tricks\\06 - Patch 'inc REG -> dec REG'", 471 | "Patches all 'inc REG -> dec REG' to nops", 472 | clean_tricks_inc_dec 473 | ) 474 | PluginCommand.register( 475 | "Clean Tricks\\07 - Patch 'xor REG,REG -> je ADDR'", 476 | "Patches all 'XOR REG,REG -> je ADDR' to jmp", 477 | clean_tricks_xor_je 478 | ) 479 | PluginCommand.register( 480 | "Clean Tricks\\08 - Patch deterministic JNE", 481 | "Patch 'mov REG,A -> add REG,B -> jne ADDR' and similar", 482 | clean_tricks_oper_before_jne 483 | ) 484 | PluginCommand.register( 485 | "Clean Tricks\\09 - Patch deterministic JNS", 486 | "Patch 'mov REG,A -> add REG,B -> jns ADDR' and similar", 487 | clean_tricks_oper_before_jns 488 | ) 489 | PluginCommand.register( 490 | "Clean Tricks\\10 - Patch all", 491 | "Run all tricks", 492 | clean_tricks_all 493 | ) 494 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginmetadataversion" : 2, 3 | "name": "Clean Tricks", 4 | "type": ["core", "ui", "architecture", "binaryview", "helper"], 5 | "api": ["python3"], 6 | "description": "This plugin removes some simple known obfuscation techniques to cut down on the tedium.", 7 | "longdescription": "Binary Ninja plugin to clean up some common obfuscation techniques (dirty tricks).

There is an empty template included as well to be able to easily add your own, because there are many many obfuscations out there. However, often they get reused or slightly modified, and then it is useful to have a starting point/example. Pull requests are very welcome.

More details available at:
https://www.janbeck.com/cybersecurity-challenges-ctfs-and-more/cleantricks-to-deal-with-dirty-tricks-binary-ninja-deobfuscation-plugin

But as a simple example of what this is for, consider the following code:



The overlapping code is hidden in the literal value, 0x5ebcbff49c3ff49. It simply increases r11, then decreases it again, then jumps to the address right behind the JZ check. I have come across a binary that uses this same trick on different registers thousands of times. This plugin allows automating this patch:



And then, after the patch, the code is much simpler. In many cases this allows binary ninja to reanalyze the program flow into a much more convienient form.

", 8 | "license": { 9 | "name": "MIT", 10 | "text": "Copyright (c) \n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." 11 | }, 12 | "platforms" : ["Darwin", "Linux", "Windows"], 13 | "installinstructions" : { 14 | "Darwin" : "", 15 | "Linux" : "", 16 | "Windows" : "" 17 | }, 18 | "dependencies": { 19 | "pip": [], 20 | "apt": [], 21 | "installers": [], 22 | "other": [] 23 | }, 24 | "version": "1.4", 25 | "author": "Jan Beck", 26 | "minimumbinaryninjaversion": 1200 27 | } 28 | -------------------------------------------------------------------------------- /readme_pics/code_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janbbeck/CleanTricks/bf0b8efbc6e27b16e72f26640fef9cc4fd4e3c44/readme_pics/code_after.png -------------------------------------------------------------------------------- /readme_pics/code_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janbbeck/CleanTricks/bf0b8efbc6e27b16e72f26640fef9cc4fd4e3c44/readme_pics/code_before.png -------------------------------------------------------------------------------- /readme_pics/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janbbeck/CleanTricks/bf0b8efbc6e27b16e72f26640fef9cc4fd4e3c44/readme_pics/menu.png --------------------------------------------------------------------------------