From 8ea67ebe4fc7b76e5d2d607a81ad64f252307636 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Tue, 9 Jun 2026 12:41:34 -0500 Subject: [PATCH 01/10] Add OS Command patch candidate report. --- chb/cmdline/chkx | 36 +++++++++++ chb/cmdline/reportcmds.py | 127 +++++++++++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 1 deletion(-) diff --git a/chb/cmdline/chkx b/chb/cmdline/chkx index 6c562c36..2a0a6f31 100755 --- a/chb/cmdline/chkx +++ b/chb/cmdline/chkx @@ -1334,6 +1334,42 @@ def parse() -> argparse.Namespace: report_patchcandidates.set_defaults(func=REP.report_patch_candidates) + # --- report patch candidates + report_oscmdcandidates = reportparsers.add_parser("oscmdcandidates") + report_oscmdcandidates.add_argument("xname", help="name of executable") + report_oscmdcandidates.add_argument( + "--output", "-o", + help="name of output file (without extension)") + report_oscmdcandidates.add_argument( + "--json", + action="store_true", + help="output results in json format") + report_oscmdcandidates.add_argument( + "--targets", + nargs="*", + default=['all'], + help="list of target library functions to include. If not passed, all " + "library functions found will be included.") + report_oscmdcandidates.add_argument( + "--verbose", "-v", + action="store_true", + help="print functions examined") + report_oscmdcandidates.add_argument( + "--loglevel", "-log", + choices=UL.LogLevel.options(), + default="NONE", + help="activate logging with the given level (default to stderr)") + report_oscmdcandidates.add_argument( + "--logfilename", + help="name of file to write log messages") + report_oscmdcandidates.add_argument( + "--logfilemode", + choices=["a", "w"], + default="a", + help="file mode for log file: append (a, default), or write (w)") + + report_oscmdcandidates.set_defaults(func=REP.report_os_cmd_candidates) + ''' # -- report application calls -- diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index 5c0015ef..da4452bc 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -49,6 +49,8 @@ from chb.app.Callgraph import Callgraph from chb.app.Instruction import Instruction +from chb.app.XPOPredicate import XPOTrustedOsCmdString + from chb.arm.ARMInstruction import ARMInstruction from chb.bctypes.BCAttrParam import BCAttrParamInt, BCAttrParamStr, BCAttrParamCons @@ -1452,7 +1454,6 @@ def include_target(target: 'CallTarget') -> bool: intermediate_attribute = find_function_attribute(app, dstarg_index, fname, instr, pc) if intermediate_attribute is None: continue - for inter in intermediate_callgraph[fname]: # For each caller to the intermediate function, lookup the corresponding buffer # and add it to the patch records. @@ -1520,3 +1521,127 @@ def include_target(target: 'CallTarget') -> bool: (len(content['patch-records']), n_calls)) exit(0) + +def report_os_cmd_candidates(args: argparse.Namespace) -> NoReturn: + + # arguments + xname = args.xname + xoutput: Optional[str] = args.output + xjson: bool = args.json + xverbose: bool = args.verbose + xtargets: List[str] = args.targets + loglevel: str = args.loglevel + logfilename: Optional[str] = args.logfilename + logfilemode: str = args.logfilemode + + try: + (path, xfile) = UC.get_path_filename(xname) + UF.check_analysis_results(path, xfile) + except UF.CHBError as e: + print(str(e.wrap())) + exit(1) + + UC.set_logging( + loglevel, + path, + logfilename=logfilename, + mode=logfilemode, + msg="report_os_cmd_patch_candidates invoked") + + xinfo = XI.XInfo() + xinfo.load(path, xfile) + + app = UC.get_app(path, xfile, xinfo) + + n_calls: int = 0 + libcalls = LibraryCallCallsites() + + include_all = xtargets == ['all'] + + def include_target(target: 'CallTarget') -> bool: + if include_all: + return True + return target.name in xtargets + + os_cmd_instruction_delegation = {} + os_cmd_construction = {} + + for (faddr, blocks) in app.call_instructions().items(): + for (baddr, instrs) in blocks.items(): + for instr in instrs: + n_calls += 1 + calltgt = instr.call_target + if include_target(calltgt): + if calltgt.is_so_target: + for po in instr.proofobligations(): + if type(po.xpo) == XPOTrustedOsCmdString and po.status.is_delegated_local: + if po.status.get_iaddr() == instr.iaddr: + os_cmd_construction[instr.iaddr] = (faddr, baddr, instr, instr.iaddr, calltgt) + else: + os_cmd_instruction_delegation[instr.iaddr] = po.status.get_iaddr() + libcalls.add_library_callsite(faddr, baddr, instr) + + chklogger.logger.debug("Number of calls: %s", n_calls) + + patchcallsites = libcalls.patch_callsites() + + function_addr = collect_known_fn_addrs(app, patchcallsites) + + content: Dict[str, Any] = {} + if xjson: + xinfodata = xinfo.to_json_result() + if xinfodata.is_ok: + content["identification"] = xinfodata.content + else: + write_json_result(xoutput, xinfodata, "patchcandidates") + + patch_records = [] + + for os_cmd_iaddr, construction_iaddr in os_cmd_instruction_delegation.items(): + if construction_iaddr not in os_cmd_construction: + continue + faddr, baddr, instr, iiaddr, calltgt = os_cmd_construction[construction_iaddr] + print(os_cmd_iaddr) + pc_content: Dict[str, Any] = {} + pc_content["annotation"] = instr.annotation + pc_content["faddr"] = faddr + pc_content["iaddr"] = instr.iaddr + pc_content["os-iaddr"] = os_cmd_iaddr + pc_content["target-function"] = calltgt.stub.summary().name + args: List[Dict[str, Any]] = [] + for arg in instr.call_arguments: + if arg.is_constant: + args.append({"type": "constant", "rep": str(arg.constant)}) + elif arg.is_stack_address: + fn = app.function(faddr) + stackframe = fn.stackframe + argoffset = arg.stack_address_offset() + buffersize, sizeorigin = calculate_buffer_size(stackframe, argoffset, instr) + args.append({"type": "pointer", "max-length": buffersize, "size-origin": sizeorigin}) + else: + args.append({"type": "unknown"}) + + pc_content["args"] = args + patch_records.append(pc_content) + + content["function-addr"] = function_addr + content["patch-records"] = patch_records + chklogger.logger.debug("Number of patch callsites: %s", len(content['patch-records'])) + + if xjson: + jcontent = JSONResult("patchcandidates", content, "ok") + write_json_result(xoutput, jcontent, "patchcandidates") + exit(0) + + for patch_record in content["patch-records"]: + print(" " + patch_record['iaddr'] + " " + patch_record['annotation']) + print(" - faddr: %s" % patch_record['faddr']) + print(" - os-iaddr: %s" % patch_record['os-iaddr']) + print(" - iaddr: %s" % patch_record['iaddr']) + print(" - target function: %s" % patch_record['target-function']) + print("") + + print("Generated %d patch records from %d library calls" % + (len(content['patch-records']), n_calls)) + + exit(0) From c3b9cd358631ba83e22688721c574eb94278cd29 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Fri, 12 Jun 2026 10:03:52 -0500 Subject: [PATCH 02/10] Separate constant types. --- chb/cmdline/reportcmds.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index da4452bc..0da138e3 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -1611,7 +1611,12 @@ def include_target(target: 'CallTarget') -> bool: args: List[Dict[str, Any]] = [] for arg in instr.call_arguments: if arg.is_constant: - args.append({"type": "constant", "rep": str(arg.constant)}) + if arg.is_string_reference: + args.append({"type": "string", "rep": str(arg.constant)}) + elif arg.is_int_constant: + args.append({"type": "int", "rep": str(arg.constant)}) + else: + args.append({"type": "constant", "rep": str(arg.constant)}) elif arg.is_stack_address: fn = app.function(faddr) stackframe = fn.stackframe From 30318353d7b42e1b342517a01189379dd1b624a7 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Fri, 12 Jun 2026 13:18:48 -0500 Subject: [PATCH 03/10] Fix mypy lints. --- chb/cmdline/reportcmds.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index 0da138e3..1ed91bae 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -1573,10 +1573,12 @@ def include_target(target: 'CallTarget') -> bool: calltgt = instr.call_target if include_target(calltgt): if calltgt.is_so_target: + so_function = cast("SOFunction", cast("StubTarget", calltgt).stub) + for po in instr.proofobligations(): if type(po.xpo) == XPOTrustedOsCmdString and po.status.is_delegated_local: if po.status.get_iaddr() == instr.iaddr: - os_cmd_construction[instr.iaddr] = (faddr, baddr, instr, instr.iaddr, calltgt) + os_cmd_construction[instr.iaddr] = (faddr, baddr, instr, instr.iaddr, so_function) else: os_cmd_instruction_delegation[instr.iaddr] = po.status.get_iaddr() libcalls.add_library_callsite(faddr, baddr, instr) @@ -1600,33 +1602,33 @@ def include_target(target: 'CallTarget') -> bool: for os_cmd_iaddr, construction_iaddr in os_cmd_instruction_delegation.items(): if construction_iaddr not in os_cmd_construction: continue - faddr, baddr, instr, iiaddr, calltgt = os_cmd_construction[construction_iaddr] + faddr, baddr, instr, iiaddr, so_function = os_cmd_construction[construction_iaddr] print(os_cmd_iaddr) pc_content: Dict[str, Any] = {} pc_content["annotation"] = instr.annotation pc_content["faddr"] = faddr pc_content["iaddr"] = instr.iaddr pc_content["os-iaddr"] = os_cmd_iaddr - pc_content["target-function"] = calltgt.stub.summary().name - args: List[Dict[str, Any]] = [] + pc_content["target-function"] = so_function.summary().name + fn_args: List[Dict[str, Any]] = [] for arg in instr.call_arguments: if arg.is_constant: if arg.is_string_reference: - args.append({"type": "string", "rep": str(arg.constant)}) + fn_args.append({"type": "string", "rep": str(arg.constant)}) elif arg.is_int_constant: - args.append({"type": "int", "rep": str(arg.constant)}) + fn_args.append({"type": "int", "rep": str(arg.constant)}) else: - args.append({"type": "constant", "rep": str(arg.constant)}) + fn_args.append({"type": "constant", "rep": str(arg.constant)}) elif arg.is_stack_address: fn = app.function(faddr) stackframe = fn.stackframe argoffset = arg.stack_address_offset() buffersize, sizeorigin = calculate_buffer_size(stackframe, argoffset, instr) - args.append({"type": "pointer", "max-length": buffersize, "size-origin": sizeorigin}) + fn_args.append({"type": "pointer", "max-length": buffersize, "size-origin": sizeorigin}) else: - args.append({"type": "unknown"}) + fn_args.append({"type": "unknown"}) - pc_content["args"] = args + pc_content["fn_args"] = fn_args patch_records.append(pc_content) content["function-addr"] = function_addr From 513d4b1864274310f340bb53f017ea7fec139181 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Mon, 15 Jun 2026 10:11:55 -0500 Subject: [PATCH 04/10] Expand to support indirect calls. --- chb/cmdline/reportcmds.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index 1ed91bae..7c12562b 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -49,7 +49,7 @@ from chb.app.Callgraph import Callgraph from chb.app.Instruction import Instruction -from chb.app.XPOPredicate import XPOTrustedOsCmdString +from chb.app.XPOPredicate import XPOTrustedOsCmdFmtString, XPOTrustedOsCmdString from chb.arm.ARMInstruction import ARMInstruction @@ -1563,8 +1563,13 @@ def include_target(target: 'CallTarget') -> bool: return True return target.name in xtargets - os_cmd_instruction_delegation = {} - os_cmd_construction = {} + + # Track every call with a trusted-os-cmd-fmt-string proofobligation as potential + # patch sites for command injection vulnerabilities. We additionally track all + # the trusted-os-cmd-string delegations as potential sites where the system + # (or equivalent) function is called to allow for filtering on those addresses. + os_cmd_construction: Dict[str, tuple[str, "Instruction", "SOFunction"]] = {} + os_cmd_delegations: Dict[str,List[str]] = defaultdict(list) for (faddr, blocks) in app.call_instructions().items(): for (baddr, instrs) in blocks.items(): @@ -1574,13 +1579,13 @@ def include_target(target: 'CallTarget') -> bool: if include_target(calltgt): if calltgt.is_so_target: so_function = cast("SOFunction", cast("StubTarget", calltgt).stub) - for po in instr.proofobligations(): - if type(po.xpo) == XPOTrustedOsCmdString and po.status.is_delegated_local: - if po.status.get_iaddr() == instr.iaddr: - os_cmd_construction[instr.iaddr] = (faddr, baddr, instr, instr.iaddr, so_function) - else: - os_cmd_instruction_delegation[instr.iaddr] = po.status.get_iaddr() + if type(po.xpo) == XPOTrustedOsCmdString: + if po.status.is_delegated_local and po.status.get_iaddr() != instr.iaddr: + os_cmd_delegations[po.status.get_iaddr()].append(instr.iaddr) + if type(po.xpo) == XPOTrustedOsCmdFmtString: + if po.status.is_delegated_local and po.status.get_iaddr() == instr.iaddr: + os_cmd_construction[instr.iaddr] = (faddr, instr, so_function) libcalls.add_library_callsite(faddr, baddr, instr) chklogger.logger.debug("Number of calls: %s", n_calls) @@ -1599,17 +1604,15 @@ def include_target(target: 'CallTarget') -> bool: patch_records = [] - for os_cmd_iaddr, construction_iaddr in os_cmd_instruction_delegation.items(): - if construction_iaddr not in os_cmd_construction: - continue - faddr, baddr, instr, iiaddr, so_function = os_cmd_construction[construction_iaddr] - print(os_cmd_iaddr) + for construction_iaddr in os_cmd_construction.keys(): + faddr, instr, so_function = os_cmd_construction[construction_iaddr] pc_content: Dict[str, Any] = {} pc_content["annotation"] = instr.annotation pc_content["faddr"] = faddr pc_content["iaddr"] = instr.iaddr - pc_content["os-iaddr"] = os_cmd_iaddr - pc_content["target-function"] = so_function.summary().name + if instr.iaddr in os_cmd_delegations: + pc_content["os-iaddrs"] = os_cmd_delegations[instr.iaddr] + pc_content["target-function"] = so_function.name fn_args: List[Dict[str, Any]] = [] for arg in instr.call_arguments: if arg.is_constant: @@ -1643,7 +1646,8 @@ def include_target(target: 'CallTarget') -> bool: for patch_record in content["patch-records"]: print(" " + patch_record['iaddr'] + " " + patch_record['annotation']) print(" - faddr: %s" % patch_record['faddr']) - print(" - os-iaddr: %s" % patch_record['os-iaddr']) + if 'os-iaddrs' in patch_record: + print(" - os-iaddrs: %s" % ' '.join([str(i) for i in patch_record['os-iaddrs']])) print(" - iaddr: %s" % patch_record['iaddr']) print(" - target function: %s" % patch_record['target-function']) print("") From 0fdb0562d7fd715abc8f50d014d1502b97778b99 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Tue, 16 Jun 2026 09:48:05 -0500 Subject: [PATCH 05/10] Resolve review comments and switch os-iaddr to exec-iaddr. --- chb/cmdline/reportcmds.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index 7c12562b..b23a1c9e 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -76,6 +76,7 @@ from chb.api.FunctionStub import SOFunction from chb.app.AppAccess import AppAccess from chb.app.BasicBlock import BasicBlock + from chb.app.FnProofObligations import ProofObligation from chb.app.Function import Function from chb.app.FunctionStackframe import FunctionStackframe from chb.app.Instruction import Instruction @@ -1563,12 +1564,21 @@ def include_target(target: 'CallTarget') -> bool: return True return target.name in xtargets + def is_cmd_delegation(po: 'ProofObligation') -> bool: + if type(po.xpo) == XPOTrustedOsCmdString: + return po.status.is_delegated_local and po.status.get_iaddr() != instr.iaddr + return False + + def is_cmd_construction(po: 'ProofObligation') -> bool: + if type(po.xpo) == XPOTrustedOsCmdFmtString: + return po.status.is_delegated_local and po.status.get_iaddr() == instr.iaddr + return False # Track every call with a trusted-os-cmd-fmt-string proofobligation as potential # patch sites for command injection vulnerabilities. We additionally track all # the trusted-os-cmd-string delegations as potential sites where the system # (or equivalent) function is called to allow for filtering on those addresses. - os_cmd_construction: Dict[str, tuple[str, "Instruction", "SOFunction"]] = {} + os_cmd_construction: List[tuple[str, "Instruction", "SOFunction"]] = [] os_cmd_delegations: Dict[str,List[str]] = defaultdict(list) for (faddr, blocks) in app.call_instructions().items(): @@ -1580,12 +1590,10 @@ def include_target(target: 'CallTarget') -> bool: if calltgt.is_so_target: so_function = cast("SOFunction", cast("StubTarget", calltgt).stub) for po in instr.proofobligations(): - if type(po.xpo) == XPOTrustedOsCmdString: - if po.status.is_delegated_local and po.status.get_iaddr() != instr.iaddr: - os_cmd_delegations[po.status.get_iaddr()].append(instr.iaddr) - if type(po.xpo) == XPOTrustedOsCmdFmtString: - if po.status.is_delegated_local and po.status.get_iaddr() == instr.iaddr: - os_cmd_construction[instr.iaddr] = (faddr, instr, so_function) + if is_cmd_delegation(po): + os_cmd_delegations[po.status.get_iaddr()].append(instr.iaddr) + if is_cmd_construction(po): + os_cmd_construction.append((faddr, instr, so_function)) libcalls.add_library_callsite(faddr, baddr, instr) chklogger.logger.debug("Number of calls: %s", n_calls) @@ -1604,14 +1612,18 @@ def include_target(target: 'CallTarget') -> bool: patch_records = [] - for construction_iaddr in os_cmd_construction.keys(): - faddr, instr, so_function = os_cmd_construction[construction_iaddr] + for faddr, instr, so_function in os_cmd_construction: pc_content: Dict[str, Any] = {} pc_content["annotation"] = instr.annotation pc_content["faddr"] = faddr pc_content["iaddr"] = instr.iaddr + # If the command execution and construction are separate patch sites, + # we add them as exec-iaddrs to allow for filtering, otherwise we add + # the current site as the exec-iaddrs. if instr.iaddr in os_cmd_delegations: - pc_content["os-iaddrs"] = os_cmd_delegations[instr.iaddr] + pc_content["exec-iaddrs"] = os_cmd_delegations[instr.iaddr] + else: + pc_content["exec-iaddrs"] = [instr.iaddr] pc_content["target-function"] = so_function.name fn_args: List[Dict[str, Any]] = [] for arg in instr.call_arguments: @@ -1629,6 +1641,8 @@ def include_target(target: 'CallTarget') -> bool: buffersize, sizeorigin = calculate_buffer_size(stackframe, argoffset, instr) fn_args.append({"type": "pointer", "max-length": buffersize, "size-origin": sizeorigin}) else: + # Unable to derive the type of the argument, additional analysis may + # be able to derive it based on the format string. fn_args.append({"type": "unknown"}) pc_content["fn_args"] = fn_args @@ -1646,8 +1660,7 @@ def include_target(target: 'CallTarget') -> bool: for patch_record in content["patch-records"]: print(" " + patch_record['iaddr'] + " " + patch_record['annotation']) print(" - faddr: %s" % patch_record['faddr']) - if 'os-iaddrs' in patch_record: - print(" - os-iaddrs: %s" % ' '.join([str(i) for i in patch_record['os-iaddrs']])) + print(" - exec-iaddrs: %s" % ' '.join([str(i) for i in patch_record['exec-iaddrs']])) print(" - iaddr: %s" % patch_record['iaddr']) print(" - target function: %s" % patch_record['target-function']) print("") From e3dd60fcfc45aebd944841ab72886c161efc7bf1 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Tue, 16 Jun 2026 13:05:37 -0500 Subject: [PATCH 06/10] Add support for app function calls. --- chb/cmdline/reportcmds.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index b23a1c9e..71c5cd65 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -1569,17 +1569,14 @@ def is_cmd_delegation(po: 'ProofObligation') -> bool: return po.status.is_delegated_local and po.status.get_iaddr() != instr.iaddr return False - def is_cmd_construction(po: 'ProofObligation') -> bool: - if type(po.xpo) == XPOTrustedOsCmdFmtString: - return po.status.is_delegated_local and po.status.get_iaddr() == instr.iaddr - return False - # Track every call with a trusted-os-cmd-fmt-string proofobligation as potential # patch sites for command injection vulnerabilities. We additionally track all # the trusted-os-cmd-string delegations as potential sites where the system # (or equivalent) function is called to allow for filtering on those addresses. - os_cmd_construction: List[tuple[str, "Instruction", "SOFunction"]] = [] + os_cmd_construction: List[tuple[str, "Instruction", str]] = [] os_cmd_delegations: Dict[str,List[str]] = defaultdict(list) + app_functions: Dict[str, str] = {} + for (faddr, blocks) in app.call_instructions().items(): for (baddr, instrs) in blocks.items(): @@ -1587,20 +1584,23 @@ def is_cmd_construction(po: 'ProofObligation') -> bool: n_calls += 1 calltgt = instr.call_target if include_target(calltgt): - if calltgt.is_so_target: - so_function = cast("SOFunction", cast("StubTarget", calltgt).stub) - for po in instr.proofobligations(): - if is_cmd_delegation(po): - os_cmd_delegations[po.status.get_iaddr()].append(instr.iaddr) - if is_cmd_construction(po): - os_cmd_construction.append((faddr, instr, so_function)) - libcalls.add_library_callsite(faddr, baddr, instr) + for po in instr.proofobligations(): + if is_cmd_delegation(po): + os_cmd_delegations[po.status.get_iaddr()].append(instr.iaddr) + if type(po.xpo) == XPOTrustedOsCmdFmtString: + os_cmd_construction.append((faddr, instr, calltgt.name)) + if calltgt.is_app_target and calltgt.name not in app_functions: + app_functions[calltgt.name] = str(calltgt.address) + if calltgt.is_so_target: + libcalls.add_library_callsite(faddr, baddr, instr) chklogger.logger.debug("Number of calls: %s", n_calls) patchcallsites = libcalls.patch_callsites() function_addr = collect_known_fn_addrs(app, patchcallsites) + for name, addr in app_functions.items(): + function_addr[name] = addr content: Dict[str, Any] = {} if xjson: @@ -1612,7 +1612,7 @@ def is_cmd_construction(po: 'ProofObligation') -> bool: patch_records = [] - for faddr, instr, so_function in os_cmd_construction: + for faddr, instr, target_function in os_cmd_construction: pc_content: Dict[str, Any] = {} pc_content["annotation"] = instr.annotation pc_content["faddr"] = faddr @@ -1624,7 +1624,7 @@ def is_cmd_construction(po: 'ProofObligation') -> bool: pc_content["exec-iaddrs"] = os_cmd_delegations[instr.iaddr] else: pc_content["exec-iaddrs"] = [instr.iaddr] - pc_content["target-function"] = so_function.name + pc_content["target-function"] = target_function fn_args: List[Dict[str, Any]] = [] for arg in instr.call_arguments: if arg.is_constant: From c1a838c82cdf5de260eb8bcf430ce2183fd8e765 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Tue, 16 Jun 2026 13:23:17 -0500 Subject: [PATCH 07/10] Fix lint --- chb/cmdline/reportcmds.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index 71c5cd65..01eab216 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -72,7 +72,7 @@ if TYPE_CHECKING: from chb.api.CallTarget import ( - StubTarget, CallTarget) + StubTarget, AppTarget, CallTarget) from chb.api.FunctionStub import SOFunction from chb.app.AppAccess import AppAccess from chb.app.BasicBlock import BasicBlock @@ -1590,7 +1590,7 @@ def is_cmd_delegation(po: 'ProofObligation') -> bool: if type(po.xpo) == XPOTrustedOsCmdFmtString: os_cmd_construction.append((faddr, instr, calltgt.name)) if calltgt.is_app_target and calltgt.name not in app_functions: - app_functions[calltgt.name] = str(calltgt.address) + app_functions[calltgt.name] = str(cast('AppTarget', calltgt).address) if calltgt.is_so_target: libcalls.add_library_callsite(faddr, baddr, instr) From 6a621ff51f9afc15adccdbe2abe042754ef04ce3 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Tue, 16 Jun 2026 15:48:54 -0500 Subject: [PATCH 08/10] Fix comment --- chb/cmdline/reportcmds.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index 01eab216..3222a24b 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -1570,14 +1570,15 @@ def is_cmd_delegation(po: 'ProofObligation') -> bool: return False # Track every call with a trusted-os-cmd-fmt-string proofobligation as potential - # patch sites for command injection vulnerabilities. We additionally track all - # the trusted-os-cmd-string delegations as potential sites where the system - # (or equivalent) function is called to allow for filtering on those addresses. + # patch sites for command injection vulnerabilities. These correspond to the site + # where the format string is constructed. We additionally track all the + # trusted-os-cmd-string delegations as potential sites where the actual command + # execution occurs, and allows consumers to filter based on those instruction + # addresses. os_cmd_construction: List[tuple[str, "Instruction", str]] = [] os_cmd_delegations: Dict[str,List[str]] = defaultdict(list) app_functions: Dict[str, str] = {} - for (faddr, blocks) in app.call_instructions().items(): for (baddr, instrs) in blocks.items(): for instr in instrs: From 6032402a780a227ca3debb9d194f4d17b37a4a53 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Fri, 19 Jun 2026 15:49:11 -0500 Subject: [PATCH 09/10] Use FormatStringSpecifier to determine roles of arguments in command injection patches. --- chb/api/FormatStringSpec.py | 10 ++++++++++ chb/cmdline/reportcmds.py | 39 ++++++++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/chb/api/FormatStringSpec.py b/chb/api/FormatStringSpec.py index dd40f880..81cdc974 100644 --- a/chb/api/FormatStringSpec.py +++ b/chb/api/FormatStringSpec.py @@ -110,6 +110,16 @@ def lengthmodifier(self) -> str: def conversion(self) -> str: return self.tags[3] + @property + def arg_type(self) -> str: + if self.conversion == 's': + return 'string' + elif self.conversion == 'd': + return 'int' + elif self.conversion == 'f': + return 'float' + return 'unknown' + @property def flags(self) -> str: return "".join(chr(i) for i in self.args) diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index 3222a24b..48ca4f58 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -1627,24 +1627,43 @@ def is_cmd_delegation(po: 'ProofObligation') -> bool: pc_content["exec-iaddrs"] = [instr.iaddr] pc_content["target-function"] = target_function fn_args: List[Dict[str, Any]] = [] + fmt_string, fmt_string_specs = app.function(faddr).formatstrings[instr.iaddr] + pc_content["format-string"] = fmt_string + found_fmt_arg = False + fmt_arg_count = 0 for arg in instr.call_arguments: + fn_arg: Dict[str, Any] = {"type": "unknown", "role": "unknown"} if arg.is_constant: - if arg.is_string_reference: - fn_args.append({"type": "string", "rep": str(arg.constant)}) - elif arg.is_int_constant: - fn_args.append({"type": "int", "rep": str(arg.constant)}) + fn_arg["rep"] = str(arg.constant) + if arg.is_int_constant: + fn_arg["type"] = "int" + elif arg.is_string_reference: + fn_arg["type"] = "string" else: - fn_args.append({"type": "constant", "rep": str(arg.constant)}) + fn_arg["type"] = "constant" elif arg.is_stack_address: fn = app.function(faddr) stackframe = fn.stackframe argoffset = arg.stack_address_offset() buffersize, sizeorigin = calculate_buffer_size(stackframe, argoffset, instr) - fn_args.append({"type": "pointer", "max-length": buffersize, "size-origin": sizeorigin}) + fn_arg["type"] = "pointer" + fn_arg["max-length"] = buffersize + fn_arg["size-origin"] = sizeorigin + + # If we've already found the format string input, treat the remaining arguments + # up to the number of format string specifiers as inputs. + if found_fmt_arg: + fn_arg["role"] = "input" + if fmt_arg_count < len(fmt_string_specs.argspecs): + fn_arg["type"] = fmt_string_specs.argspecs[fmt_arg_count].arg_type + fmt_arg_count += 1 else: - # Unable to derive the type of the argument, additional analysis may - # be able to derive it based on the format string. - fn_args.append({"type": "unknown"}) + fn_arg["role"] = "passthrough" + if arg.is_constant and arg.is_string_reference: + fn_arg["type"] = "string" + fn_arg["role"] = "format" + found_fmt_arg = True + fn_args.append(fn_arg) pc_content["fn_args"] = fn_args patch_records.append(pc_content) @@ -1664,6 +1683,8 @@ def is_cmd_delegation(po: 'ProofObligation') -> bool: print(" - exec-iaddrs: %s" % ' '.join([str(i) for i in patch_record['exec-iaddrs']])) print(" - iaddr: %s" % patch_record['iaddr']) print(" - target function: %s" % patch_record['target-function']) + print(" - format string: %s" % patch_record['format-string']) + print(" - args: %s" % (",".join(['%s(%s)' % (a['role'], a['type']) for a in patch_record['fn_args']]))) print("") print("Generated %d patch records from %d library calls" % From 49530aeda8f93eeba3ca9b1ff9b98c9daa6211c8 Mon Sep 17 00:00:00 2001 From: Steven Valdez Date: Fri, 19 Jun 2026 15:52:18 -0500 Subject: [PATCH 10/10] Add rep. --- chb/cmdline/reportcmds.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chb/cmdline/reportcmds.py b/chb/cmdline/reportcmds.py index 48ca4f58..c865dae1 100644 --- a/chb/cmdline/reportcmds.py +++ b/chb/cmdline/reportcmds.py @@ -1632,9 +1632,8 @@ def is_cmd_delegation(po: 'ProofObligation') -> bool: found_fmt_arg = False fmt_arg_count = 0 for arg in instr.call_arguments: - fn_arg: Dict[str, Any] = {"type": "unknown", "role": "unknown"} + fn_arg: Dict[str, Any] = {"type": "unknown", "role": "unknown", "rep": str(arg)} if arg.is_constant: - fn_arg["rep"] = str(arg.constant) if arg.is_int_constant: fn_arg["type"] = "int" elif arg.is_string_reference: