IDA Scripts for Windows Kernel Driver Analysis - DriverShield
IDAPython scripts by Tolga SEZER for statically extracting IOCTL control codes from Windows .sys drivers and correlating them to sensitive kernel sink calls in IDA Pro. Reverse-engineering automation for driver triage.
IDA Scripts for Malicious IOCTL Key Extraction
IDAPython scripts by Tolga SEZER for reverse engineering Windows kernel-mode drivers (.sys) in IDA Pro. Each script statically extracts driver IOCTL control codes and correlates them to the sensitive kernel sink calls they can reach - static extraction and triage only, with no scanning, patching, or execution.
IOCTL Sink-Correlation Extractor - Tolga SEZER
Extracts a driver's IOCTL control codes from its .sys binary in IDA and maps each code, through the dispatch call graph, to the sensitive kernel sink calls it can reach (process, virtual-memory, section, and file operations). Static extraction and correlation only - it surfaces which IOCTL keys expose sensitive primitives, with no scanning, patching, or execution.
import ida_bytes
import ida_funcs
import ida_ida
import ida_idaapi
import ida_lines
import ida_name
import ida_segment
import ida_ua
import ida_idp
import idautils
# Author: Tolga SEZER (https://www.linkedin.com/in/tolgasezer-com-tr)
# drivershield.io
CALLGRAPH_DEPTH = 7
IOCTL_MIN = 0x80000000
IOCTL_MAX = 0xFFFFFFFF
SINK_NAMES = [
"ZwTerminateProcess", "NtTerminateProcess",
"ZwOpenProcess", "NtOpenProcess",
"PsLookupProcessByProcessId",
"ObOpenObjectByPointer", "ObReferenceObjectByHandle",
"MmCopyVirtualMemory",
"ZwWriteVirtualMemory", "NtWriteVirtualMemory",
"ZwProtectVirtualMemory", "NtProtectVirtualMemory",
"ZwMapViewOfSection", "NtMapViewOfSection",
"ZwCreateSection", "NtCreateSection",
"ZwSetSystemInformation", "NtSetSystemInformation",
"ZwLoadDriver", "NtLoadDriver",
"ZwCreateFile", "NtCreateFile",
"ZwDeviceIoControlFile", "NtDeviceIoControlFile",
]
SINK_LOWER = [s.lower() for s in SINK_NAMES]
USER_INPUT_HINTS = [
"systembuffer", "userbuffer", "type3inputbuffer", "mdladdress",
"associatedirp", "iostacklocation", "currentstacklocation",
"inputbufferlength", "outputbufferlength", "controlcode", "deviceiocontrol",
"iogetcurrentirpstacklocation",
"probeforread", "probeforwrite", "mmprobeandlockpages",
]
def is_x64():
return ida_ida.inf_is_64bit()
def ea_name(ea):
n = ida_name.get_ea_name(ea)
return n if n else ""
def disasm(ea):
s = ida_lines.generate_disasm_line(ea, 0)
return ida_lines.tag_remove(s).lower() if s else ""
def is_exec_seg(ea):
seg = ida_segment.getseg(ea)
return seg and (seg.perm & ida_segment.SEGPERM_EXEC)
def iter_code_eas():
for s in idautils.Segments():
if not is_exec_seg(s):
continue
seg = ida_segment.getseg(s)
for ea in idautils.Heads(seg.start_ea, seg.end_ea):
if ida_bytes.is_code(ida_bytes.get_flags(ea)):
yield ea
def looks_like_ioctl(v):
v &= 0xFFFFFFFF
return IOCTL_MIN <= v <= IOCTL_MAX
def decode_ioctl(v):
v &= 0xFFFFFFFF
dev = (v >> 16) & 0xFFFF
acc = (v >> 14) & 0x3
fun = (v >> 2) & 0xFFF
mth = v & 0x3
return dev, fun, mth, acc
def mth_name(m):
return ("BUFFERED", "IN_DIRECT", "OUT_DIRECT", "NEITHER")[m] if 0 <= m <= 3 else "?"
def acc_name(a):
return ("ANY", "READ", "WRITE", "READWRITE")[a] if 0 <= a <= 3 else "?"
def vbhex32(v):
return "&H%08X" % (v & 0xFFFFFFFF)
def get_call_name(ea):
insn = ida_ua.insn_t()
if ida_ua.decode_insn(insn, ea) == 0:
return None, None
if ida_ua.print_insn_mnem(ea).lower() != "call":
return None, None
op = insn.ops[0]
tgt = None
nm = ""
if op.type in (ida_ua.o_near, ida_ua.o_far, ida_ua.o_mem):
tgt = op.addr
nm = ea_name(tgt).lower()
if not nm:
line = disasm(ea)
for s in SINK_LOWER:
if s in line:
return None, s
return tgt, ""
return tgt, nm
def is_sink_call(ea):
_, nm = get_call_name(ea)
if not nm:
return None
for s in SINK_LOWER:
if s in nm:
return s
return None
def find_sink_calls():
hits = []
for ea in iter_code_eas():
if ida_ua.print_insn_mnem(ea).lower() != "call":
continue
s = is_sink_call(ea)
if not s:
continue
f = ida_funcs.get_func(ea)
if not f:
continue
hits.append((s, ea, f.start_ea))
return hits
def collect_ioctl_immediates():
val_to_eas = {}
for ea in iter_code_eas():
insn = ida_ua.insn_t()
if ida_ua.decode_insn(insn, ea) == 0:
continue
for op in insn.ops:
if op.type == ida_ua.o_imm:
v = op.value & 0xFFFFFFFF
if looks_like_ioctl(v):
val_to_eas.setdefault(v, set()).add(ea)
return val_to_eas
def choose_ioctl_devs(ioctl_vals):
dev_counts = {}
for v in ioctl_vals:
dev = (v >> 16) & 0xFFFF
dev_counts[dev] = dev_counts.get(dev, 0) + 1
deny = set([0xC000, 0x8000, 0xFFFF])
good = [(d, c) for (d, c) in dev_counts.items() if d not in deny]
if not good:
return set()
good.sort(key=lambda x: x[1], reverse=True)
chosen = set()
for d, c in good:
if c >= 3:
chosen.add(d)
if not chosen:
chosen.add(good[0][0])
return chosen
def build_func_to_ioctls(val_to_eas, candidate_devs):
func_to_ioctls = {}
for v, eas in val_to_eas.items():
dev = (v >> 16) & 0xFFFF
if candidate_devs and dev not in candidate_devs:
continue
for ea in eas:
f = ida_funcs.get_func(ea)
if not f:
continue
func_to_ioctls.setdefault(f.start_ea, set()).add(v)
return func_to_ioctls
def func_has_user_input(fea):
f = ida_funcs.get_func(fea)
if not f:
return False
for ea in idautils.FuncItems(f.start_ea):
line = disasm(ea)
for h in USER_INPUT_HINTS:
if h in line:
return True
if ida_ua.print_insn_mnem(ea).lower() == "call":
_, nm = get_call_name(ea)
if nm:
for h in USER_INPUT_HINTS:
if h in nm:
return True
return False
def callers_of_func(fea):
out = set()
for x in idautils.CodeRefsTo(fea, 0):
cf = ida_funcs.get_func(x)
if cf:
out.add(cf.start_ea)
return out
def backward_bfs(start_fea, depth):
q = [(start_fea, 0)]
seen = set([start_fea])
parent = {}
dist = {start_fea: 0}
while q:
cur, d = q.pop(0)
if d >= depth:
continue
for c in callers_of_func(cur):
if c in seen:
continue
seen.add(c)
parent[c] = cur
dist[c] = d + 1
q.append((c, d + 1))
return seen, parent, dist
def path_to_sink(node, parent):
p = [node]
cur = node
while cur in parent:
cur = parent[cur]
p.append(cur)
return p
def fmt_path(p):
parts = []
for fea in p:
nm = ea_name(fea)
if not nm:
nm = "sub_%X" % fea
parts.append("0x%X(%s)" % (fea, nm))
return " -> ".join(parts)
def sinks_in_func(fea):
f = ida_funcs.get_func(fea)
if not f:
return set()
ss = set()
for ea in idautils.FuncItems(f.start_ea):
if ida_ua.print_insn_mnem(ea).lower() != "call":
continue
s = is_sink_call(ea)
if s:
ss.add(s)
return ss
def main():
print("\n=== SINK-CENTERED BACKWARD IOCTL ANALYSIS (IDA 9.x) ===\n")
print("arch =", "x64" if is_x64() else "x86")
print("callgraph_depth =", CALLGRAPH_DEPTH)
val_to_eas = collect_ioctl_immediates()
print("raw_ioctl_like_imms =", len(val_to_eas))
candidate_devs = choose_ioctl_devs(list(val_to_eas.keys()))
if candidate_devs:
print("candidate_dev_types =", ",".join(["0x%04X" % d for d in sorted(candidate_devs)]))
else:
print("candidate_dev_types = (none)")
func_to_ioctls = build_func_to_ioctls(val_to_eas, candidate_devs)
sink_calls = find_sink_calls()
print("sink_calls_found =", len(sink_calls))
sink_instances = []
for s, call_ea, sink_func in sink_calls:
sink_instances.append((s, call_ea, sink_func))
if not sink_instances:
print("\n[-] Sink bulunamadı. (Import isimleri yoksa disasm üzerinden genişletmek gerekir)\n")
return
user_cache = {}
def get_user(fea):
if fea in user_cache:
return user_cache[fea]
v = func_has_user_input(fea)
user_cache[fea] = v
return v
ioctl_to_findings = {}
sink_reports = []
for s, call_ea, sink_func in sink_instances:
seen, parent, dist = backward_bfs(sink_func, CALLGRAPH_DEPTH)
closure_ioctls = set()
closure_user = False
best_node = None
best_d = 10**9
for fea in seen:
if fea in func_to_ioctls:
closure_ioctls |= func_to_ioctls[fea]
d = dist.get(fea, 10**9)
if func_to_ioctls[fea] and d < best_d:
best_d = d
best_node = fea
if get_user(fea):
closure_user = True
best_path = None
if best_node is not None:
best_path = path_to_sink(best_node, parent)
for v in closure_ioctls:
ioctl_to_findings.setdefault(v, []).append({
"sink": s,
"sink_func": sink_func,
"call_ea": call_ea,
"has_user": closure_user,
"evidence_func": best_node if best_node is not None else sink_func,
})
sink_reports.append({
"sink": s,
"call_ea": call_ea,
"sink_func": sink_func,
"closure_ioctls": closure_ioctls,
"closure_user": closure_user,
"best_node": best_node,
"best_path": best_path,
})
print("\n=== IOCTL -> SINK EŞLEŞME RAPORU ===\n")
reported_ioctls = 0
for v in sorted(ioctl_to_findings.keys()):
dev, fun, mth, acc = decode_ioctl(v)
sinks = sorted(list(set([f["sink"] for f in ioctl_to_findings[v]])))
user_any = any([f["has_user"] for f in ioctl_to_findings[v]])
ev = None
for f in ioctl_to_findings[v]:
if f.get("evidence_func") is not None:
ev = f["evidence_func"]
break
evn = ea_name(ev) if ev else ""
if not evn and ev:
evn = "sub_%X" % ev
print("%s method=%s access=%s dev=&H%04X func=&H%03X" % (
vbhex32(v), mth_name(mth), acc_name(acc), dev, fun
))
print(" sinks =", ",".join(sinks) if sinks else "none")
print(" user_input =", "YES" if user_any else "NO")
if ev:
print(" evidence = 0x%X (%s)" % (ev, evn))
print("")
reported_ioctls += 1
print("reported_ioctls =", reported_ioctls)
print("\n=== SINK MERKEZLİ DETAY (PATH) ===\n")
for r in sink_reports:
sf = r["sink_func"]
sfn = ea_name(sf)
if not sfn:
sfn = "sub_%X" % sf
print("SINK:", r["sink"])
print(" callsite = 0x%X" % r["call_ea"])
print(" sink_func = 0x%X (%s)" % (sf, sfn))
print(" user_input =", "YES" if r["closure_user"] else "NO")
if r["closure_ioctls"]:
lst = sorted(list(r["closure_ioctls"]))[:30]
print(" ioctls =", ",".join([vbhex32(x) for x in lst]) + (" ..." if len(r["closure_ioctls"]) > 30 else ""))
else:
print(" ioctls = none")
if r["best_path"]:
print(" best_path =", fmt_path(r["best_path"]))
else:
print(" best_path = (ioctl-bearing caller not found in depth)")
print("")
print("\n=== GLOBAL SINK SUMMARY ===\n")
global_sink_funcs = {}
for s, call_ea, sink_func in sink_instances:
global_sink_funcs.setdefault(sink_func, set()).add(s)
for fea in sorted(global_sink_funcs.keys()):
nm = ea_name(fea)
if not nm:
nm = "sub_%X" % fea
print("FUNC 0x%X (%s)" % (fea, nm))
print(" sinks:", ",".join(sorted(global_sink_funcs[fea])))
print(" user_input:", "YES" if get_user(fea) else "NO")
if fea in func_to_ioctls and func_to_ioctls[fea]:
print(" ioctls:", ",".join([vbhex32(x) for x in sorted(func_to_ioctls[fea])[:30]]))
else:
print(" ioctls: none")
print("")
main()
Simple IOCTL Extractor - Tolga SEZER
A minimal, focused extractor: scans a loaded .sys driver for immediate values that look like IOCTL control codes, decodes each into device type, function, method, and access, and prints a sorted table with a reference count. Static analysis only - no scanning, patching, or execution.
# Simple IOCTL Extractor for Windows Kernel Drivers (IDAPython, IDA 9.x)
# Author: Tolga SEZER (https://www.linkedin.com/in/tolgasezer-com-tr)
# drivershield.io
#
# Scans a loaded .sys driver for immediate values that look like IOCTL control
# codes, decodes each into DeviceType / Function / Method / Access, and prints a
# sorted table with the number of references. Static extraction only.
#
# CTL_CODE(DeviceType, Function, Method, Access):
# (DeviceType << 16) | (Access << 14) | (Function << 2) | Method
import idautils
import ida_ua
import ida_bytes
import ida_segment
METHODS = ["METHOD_BUFFERED", "METHOD_IN_DIRECT", "METHOD_OUT_DIRECT", "METHOD_NEITHER"]
ACCESS = ["FILE_ANY_ACCESS", "FILE_READ_ACCESS", "FILE_WRITE_ACCESS", "READ_WRITE"]
def decode_ioctl(v):
device = (v >> 16) & 0xFFFF
access = (v >> 14) & 0x3
function = (v >> 2) & 0xFFF
method = v & 0x3
return device, function, method, access
def looks_like_ioctl(v):
v &= 0xFFFFFFFF
device = (v >> 16) & 0xFFFF
# FILE_DEVICE_UNKNOWN (0x22) and the custom vendor range (>= 0x8000) cover the
# vast majority of driver IOCTLs. Require a device type and a non-zero function.
return (device == 0x22 or device >= 0x8000) and ((v >> 2) & 0xFFF) != 0
def exec_segments():
for ea in idautils.Segments():
seg = ida_segment.getseg(ea)
if seg and (seg.perm & ida_segment.SEGPERM_EXEC):
yield seg
def extract_ioctls():
found = {}
for seg in exec_segments():
for head in idautils.Heads(seg.start_ea, seg.end_ea):
if not ida_bytes.is_code(ida_bytes.get_flags(head)):
continue
insn = ida_ua.insn_t()
if ida_ua.decode_insn(insn, head) == 0:
continue
for op in insn.ops:
if op.type == ida_ua.o_imm and looks_like_ioctl(op.value):
found.setdefault(op.value & 0xFFFFFFFF, []).append(head)
return found
def main():
print("\n=== SIMPLE IOCTL EXTRACTOR - drivershield.io ===\n")
found = extract_ioctls()
if not found:
print("[-] No IOCTL-like control codes found.")
return
print("%-12s %-8s %-9s %-18s %-17s %s" %
("IOCTL", "DEVICE", "FUNCTION", "METHOD", "ACCESS", "REFS"))
print("-" * 78)
for v in sorted(found):
device, function, method, access = decode_ioctl(v)
print("0x%08X 0x%04X 0x%03X %-18s %-17s %d" % (
v, device, function, METHODS[method], ACCESS[access], len(found[v])))
print("\n[+] %d unique IOCTL code(s) extracted." % len(found))
main()
Function Name Extractor - Tolga SEZER
Enumerates every function in the loaded binary and prints its address, size, flags (library / thunk / static), and name - demangled when possible - while counting how many carry meaningful names versus auto-generated sub_* labels. Static extraction only.
# Function Name Extractor for Windows Kernel Drivers (IDAPython, IDA 9.x)
# Author: Tolga SEZER (https://www.linkedin.com/in/tolgasezer-com-tr)
# drivershield.io
#
# Enumerates every function in the loaded binary and prints its address, size,
# flags, and name (demangled when possible), flagging library/thunk functions
# and counting auto-generated sub_* names. Static extraction only.
import idautils
import idc
import ida_funcs
import ida_name
def flags_str(f):
tags = []
if f.flags & ida_funcs.FUNC_LIB:
tags.append("LIB")
if f.flags & ida_funcs.FUNC_THUNK:
tags.append("THUNK")
if f.flags & ida_funcs.FUNC_STATIC:
tags.append("STATIC")
return "|".join(tags) if tags else "-"
def main():
print("\n=== FUNCTION NAME EXTRACTOR - drivershield.io ===\n")
total = 0
named = 0
print("%-18s %-8s %-12s %s" % ("ADDRESS", "SIZE", "FLAGS", "NAME"))
print("-" * 80)
for ea in idautils.Functions():
f = ida_funcs.get_func(ea)
if not f:
continue
total += 1
raw = ida_name.get_ea_name(ea)
demangled = None
if raw:
demangled = ida_name.demangle_name(raw, idc.get_inf_attr(idc.INF_SHORT_DEMNAMES))
name = demangled if demangled else raw
if raw and not raw.startswith("sub_"):
named += 1
size = f.end_ea - f.start_ea
print("0x%016X %-8d %-12s %s" % (ea, size, flags_str(f), name))
print("\n[+] %d function(s) - %d named, %d auto-generated (sub_*)." %
(total, named, total - named))
main()
DriverShield © 2025-2026 · Terms · Privacy · Contact