mirror of
https://github.com/joxeankoret/diaphora
synced 2026-06-08 15:06:40 +00:00
afc31c33ae
BUG: Fixed issues #200 and #324. Also, removed all pylint directives. They are unused.
240 lines
7.2 KiB
Python
240 lines
7.2 KiB
Python
#!/usr/bin/python3
|
|
|
|
"""
|
|
Script to find patches that could be fixing vulnerabilities by simply doing
|
|
pattern matching between pseudo-codes for partial matches.
|
|
|
|
Joxean Koret
|
|
Public domain
|
|
"""
|
|
|
|
from difflib import unified_diff
|
|
|
|
from diaphora import CChooser, log
|
|
|
|
#-------------------------------------------------------------------------------
|
|
|
|
#-------------------------------------------------------------------------------
|
|
PATTERNS = [
|
|
# Generic C 'unsafe' functions
|
|
"cpy", "printf", "strcat", "strncat", "gets", "mem", "system",
|
|
"scanf", "alloc", "free", "strto",
|
|
# Windows 'unsafe' APIs
|
|
"ShellExecute", "WinExec", "LoadLibrary", "CreateProcess",
|
|
# Functions that may be interesting for Windows kernel drivers
|
|
"ProbeForWrite", "ProbeForRead",
|
|
# UNC paths related pattern
|
|
"UNC"
|
|
]
|
|
|
|
COMPARISONS = [" < ", " > ", " <= ", " >= "]
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class CVulnSearchResults:
|
|
"""
|
|
Results from a heuristic to find vulnerabilities
|
|
"""
|
|
def __init__(self):
|
|
self.found = False
|
|
self.description = None
|
|
self.line = None
|
|
|
|
#-------------------------------------------------------------------------------
|
|
SIGNED_UNSIGNED_LIST = {}
|
|
SIGNED_UNSIGNED_LIST["jl"] = "jb"
|
|
SIGNED_UNSIGNED_LIST["jle"] = "jbe"
|
|
SIGNED_UNSIGNED_LIST["jg"] = "ja"
|
|
SIGNED_UNSIGNED_LIST["jge"] = "jae"
|
|
|
|
_key = None
|
|
for _key in list(SIGNED_UNSIGNED_LIST.keys()):
|
|
value = SIGNED_UNSIGNED_LIST[_key]
|
|
SIGNED_UNSIGNED_LIST[value] = _key
|
|
del _key
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class CVulnerabilityPatches:
|
|
"""
|
|
Class used to find potentially fixed vulnerabilities by searching for patterns.
|
|
"""
|
|
def __init__(self, diaphora_obj):
|
|
""" @diaphora_obj is the CIDABinDiff object being used.
|
|
"""
|
|
self.diaphora = diaphora_obj
|
|
self.db_name = self.diaphora.db_name
|
|
|
|
self.results = []
|
|
self.dones = set()
|
|
|
|
# This is the chooser we're going to create
|
|
self.chooser = self.diaphora.chooser("Interesting matches", self.diaphora)
|
|
self.diaphora.interesting_matches = self.chooser
|
|
|
|
def before_export_function(self, ea, func_name):
|
|
return True
|
|
|
|
def after_export_function(self, d):
|
|
return d
|
|
|
|
def get_heuristics(self, category, heuristics):
|
|
return heuristics
|
|
|
|
def on_launch_heuristic(self, name, sql):
|
|
return sql
|
|
|
|
def get_queries_postfix(self, category, postfix):
|
|
return postfix
|
|
|
|
def on_finish(self):
|
|
# If we found some cool match, show this chooser
|
|
if len(self.chooser.items) > 0:
|
|
self.chooser.show(force=True)
|
|
|
|
def search_pseudo_patterns(self, src_line):
|
|
"""
|
|
Find potential vulnerabilities by simply doing pattern matching
|
|
"""
|
|
ret = CVulnSearchResults()
|
|
# Search all of our function calls patterns
|
|
for pattern in PATTERNS:
|
|
if src_line.find(pattern) > -1:
|
|
pattern = f'Pattern {repr(pattern)}'
|
|
ret.found = True
|
|
ret.description = pattern
|
|
ret.line = src_line
|
|
break
|
|
|
|
return ret
|
|
|
|
def search_for_added_size_check(self, src_line):
|
|
"""
|
|
Try to guess if it looks like a newly added size check
|
|
"""
|
|
ret = CVulnSearchResults()
|
|
if src_line.startswith("if "):
|
|
for pattern in COMPARISONS:
|
|
if src_line.find(pattern) > -1:
|
|
# Ignore comparisons with 0?
|
|
if src_line.find(f"{pattern}0 ") == -1:
|
|
ret.description = 'Potential size check added'
|
|
ret.found = True
|
|
ret.line = src_line
|
|
break
|
|
return ret
|
|
|
|
def find_vulns_using_assembly(self, func1, func2, ratio):
|
|
"""
|
|
Try to search for signedness fixed issues
|
|
"""
|
|
results = CVulnSearchResults()
|
|
|
|
# Get the assembly for both and do a typical 'diff' of both
|
|
asm1 = func1["asm"]
|
|
asm2 = func2["asm"]
|
|
if asm1 is None or asm2 is None:
|
|
return results
|
|
|
|
lines = unified_diff(asm1.split("\n"), asm2.split("\n"))
|
|
lines = list(lines)
|
|
added = None
|
|
removed = None
|
|
found = False
|
|
|
|
for line in lines:
|
|
c = line[0]
|
|
# Only consider removed/added lines (which also means modified lines)
|
|
if c in ["-", "+"]:
|
|
if c == "+":
|
|
added = line[1:]
|
|
elif c == "-":
|
|
if line[1:].endswith(":"):
|
|
continue
|
|
removed = line[1:]
|
|
|
|
if added is not None and removed is not None:
|
|
# Check the list of known signed <-> unsigned instructions
|
|
mnem1 = added.split(" ")[0].lower()
|
|
mnem2 = removed.split(" ")[0].lower()
|
|
if mnem1 in SIGNED_UNSIGNED_LIST:
|
|
if SIGNED_UNSIGNED_LIST[mnem1] == mnem2:
|
|
found = True
|
|
break
|
|
elif mnem1[0] == "b" and mnem2[0] == "b":
|
|
# Check for branch instructions where only one is signed
|
|
last_c1 = mnem1[len(mnem1)-1]
|
|
last_c2 = mnem2[len(mnem2)-1]
|
|
if (last_c1 == "s" or last_c2 == "s") and last_c1 != last_c2:
|
|
found = True
|
|
break
|
|
|
|
if found:
|
|
results.description = "Signed/Unsigned check changed"
|
|
results.found = True
|
|
results.line = f"Added: {repr(added)}\tRemoved: {repr(removed)}"
|
|
|
|
return results
|
|
|
|
def find_vulns_using_pseudocode(self, func1, func2, ratio):
|
|
results = CVulnSearchResults()
|
|
|
|
# Get the decompiled code for both and do a typical 'diff' of both
|
|
pseudo1 = func1["pseudo"]
|
|
pseudo2 = func2["pseudo"]
|
|
if pseudo1 is None or pseudo2 is None:
|
|
return results
|
|
|
|
lines = unified_diff(pseudo1.split("\n"), pseudo2.split("\n"))
|
|
for line in lines:
|
|
c = line[0]
|
|
# Only consider removed/added lines (which also means modified lines)
|
|
if c in ["-", "+"]:
|
|
src_line = line[2:].strip(" ")
|
|
|
|
results = self.search_pseudo_patterns(src_line)
|
|
if not results.found:
|
|
# Search for potentially newly added checks
|
|
if c == "+":
|
|
results = self.search_for_added_size_check(src_line)
|
|
|
|
if results.found:
|
|
break
|
|
|
|
return results
|
|
|
|
def on_match(self, func1, func2, description, ratio):
|
|
# Ignore matches with ratio 1.0, as they contain no changes
|
|
if ratio < 1.0:
|
|
name1 = func1["name"]
|
|
name2 = func2["name"]
|
|
|
|
# We might receive 2 times a match in some cases, avoid analysing it twice
|
|
key = str([name1, name2])
|
|
if key in self.dones:
|
|
return True, ratio
|
|
self.dones.add(key)
|
|
|
|
results = self.find_vulns_using_assembly(func1, func2, ratio)
|
|
if not results.found:
|
|
results = self.find_vulns_using_pseudocode(func1, func2, ratio)
|
|
|
|
# We found something that might be cool, save this function and stop
|
|
# finding more stuff for this function.
|
|
if results.found:
|
|
# Report matches while it's still finding for exciting reversers
|
|
msg = f"Potentially interesting patch found (pattern {repr(results.description)}): {name1} - {name2}"
|
|
self.diaphora.log_refresh(msg)
|
|
log(f"> {results.line}")
|
|
|
|
# And finally add the item in the chooser we created
|
|
ea1 = func1["ea"]
|
|
ea2 = func2["ea"]
|
|
bb1 = func1["nodes"]
|
|
bb2 = func2["nodes"]
|
|
item = CChooser.Item(ea1, name1, ea2, name2, results.description, ratio, bb1, bb2)
|
|
self.chooser.add_item(item)
|
|
|
|
return True, ratio
|
|
|
|
HOOKS = {"DiaphoraHooks": CVulnerabilityPatches}
|
|
CATEGORY = "PatchDiff"
|