Files
Joxean Koret afc31c33ae Bug fixes and removal of waste code
BUG: Fixed issues #200 and #324.

Also, removed all pylint directives. They are unused.
2026-05-04 19:47:57 +02:00

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"