Skip to content

Commit d8f8926

Browse files
committed
Add CIL vulnerable call detection framework
Introduces a new CodeQL pack for detecting calls to known-vulnerable methods in CIL binaries specified with data extensions. Adds abstract callable interfaces, CIL-specific implementations, query and summarization logic, model extension support, and example model data for vulnerability identification.
1 parent ce2dc37 commit d8f8926

9 files changed

Lines changed: 381 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Provides abstract classes for representing callable entities and call sites
3+
* across different binary formats (CIL, x86, etc.).
4+
*
5+
* This module defines a common interface that allows queries to work uniformly
6+
* across binary types while allowing format-specific implementations.
7+
*/
8+
9+
/**
10+
* An abstract callable entity (method, function, etc.) in a binary.
11+
* Subclasses provide format-specific implementations.
12+
*/
13+
abstract class Callable extends string {
14+
Callable() { this = this.getIdentifier() }
15+
16+
/**
17+
* Gets a unique identifier for this callable.
18+
* For CIL, this is the fully qualified method name.
19+
* For x86, this might be a symbol name or address.
20+
*/
21+
abstract string getIdentifier();
22+
23+
/**
24+
* Gets a human-readable name for this callable.
25+
*/
26+
abstract string getName();
27+
28+
/**
29+
* Gets the location of this callable in the source/binary.
30+
*/
31+
abstract Location getLocation();
32+
33+
/**
34+
* Gets a call site within this callable that calls another callable.
35+
*/
36+
abstract CallSite getACallSite();
37+
38+
/**
39+
* Holds if this callable is publicly accessible (exported, public, etc.).
40+
*/
41+
abstract predicate isPublic();
42+
}
43+
44+
/**
45+
* An abstract call site - a location where one callable invokes another.
46+
*/
47+
abstract class CallSite extends string {
48+
CallSite() { this = this.getIdentifier() }
49+
50+
/**
51+
* Gets a unique identifier for this call site.
52+
*/
53+
abstract string getIdentifier();
54+
55+
/**
56+
* Gets the callable containing this call site.
57+
*/
58+
abstract Callable getEnclosingCallable();
59+
60+
/**
61+
* Gets the target of this call, if it can be resolved.
62+
* Returns the identifier that can be matched against models.
63+
*/
64+
abstract string getCallTargetIdentifier();
65+
66+
/**
67+
* Gets the location of this call site.
68+
*/
69+
abstract Location getLocation();
70+
}
71+
72+
/**
73+
* Holds if `caller` directly calls `callee` (by identifier).
74+
*/
75+
predicate directlyCallsIdentifier(Callable caller, string calleeIdentifier) {
76+
exists(CallSite cs |
77+
cs.getEnclosingCallable() = caller and
78+
cs.getCallTargetIdentifier() = calleeIdentifier
79+
)
80+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Provides CIL-specific implementations of the Callable framework.
3+
* This module bridges CIL instructions and methods to the abstract Callable interface.
4+
*/
5+
6+
private import binary
7+
private import semmle.code.binary.ast.internal.CilInstructions
8+
9+
/**
10+
* A CIL type (class, struct, interface, etc.).
11+
*/
12+
class CilType extends @type {
13+
string toString() { result = this.getName() }
14+
15+
/** Gets the full name of this type (e.g., "System.Collections.Generic.List`1"). */
16+
string getFullName() { types(this, result, _, _) }
17+
18+
/** Gets the namespace of this type (e.g., "System.Collections.Generic"). */
19+
string getNamespace() { types(this, _, result, _) }
20+
21+
/** Gets the simple name of this type (e.g., "List`1"). */
22+
string getName() { types(this, _, _, result) }
23+
24+
/** Gets a method declared in this type. */
25+
CilMethodExt getAMethod() { result.getDeclaringType() = this }
26+
}
27+
28+
/**
29+
* A CIL method with enhanced metadata for the Callable framework.
30+
* Extends the base CilMethod with type information and qualified names.
31+
*/
32+
class CilMethodExt extends CilMethod {
33+
/** Gets the type that declares this method. */
34+
CilType getDeclaringType() { methods(this, _, _, result) }
35+
36+
/**
37+
* Gets the fully qualified name of this method in the format:
38+
* "Namespace.ClassName.MethodName"
39+
*/
40+
string getFullyQualifiedName() {
41+
exists(CilType t | t = this.getDeclaringType() |
42+
result = t.getFullName() + "." + this.getName()
43+
)
44+
}
45+
46+
/**
47+
* Holds if this method matches the given namespace, class name, and method name.
48+
*/
49+
predicate hasFullyQualifiedName(string namespace, string className, string methodName) {
50+
exists(CilType t | t = this.getDeclaringType() |
51+
t.getNamespace() = namespace and
52+
t.getName() = className and
53+
this.getName() = methodName
54+
)
55+
}
56+
57+
/** Holds if this method is publicly accessible. */
58+
predicate isPublic() {
59+
// TODO: Check actual visibility flags when available in dbscheme
60+
// For now, we can't determine visibility from IL alone
61+
any()
62+
}
63+
64+
/** Gets the location of this method. */
65+
Location getMethodLocation() {
66+
result = this.getInstruction(0).getLocation()
67+
or
68+
not exists(this.getInstruction(0)) and
69+
result instanceof EmptyLocation
70+
}
71+
}
72+
73+
/**
74+
* A CIL call instruction with enhanced target resolution.
75+
*/
76+
class CilCallExt extends CilCall {
77+
CilCallExt() { any() }
78+
79+
/**
80+
* Gets the fully qualified name of the call target.
81+
* This is extracted from il_call_target_unresolved.
82+
*/
83+
string getCallTargetFullyQualifiedName() { result = this.getExternalName() }
84+
85+
/**
86+
* Holds if this call targets a method matching the given fully qualified components.
87+
* The external name format is "Namespace.ClassName.MethodName" (e.g., "System.Console.WriteLine").
88+
*/
89+
predicate targetsMethod(string namespace, string className, string methodName) {
90+
exists(string target | target = this.getExternalName() |
91+
// Format: Namespace.ClassName.MethodName
92+
// We need to find the last two dots to split namespace, class, and method
93+
exists(int lastDot, int secondLastDot, string nsAndClass |
94+
lastDot = max(int i | target.charAt(i) = ".") and
95+
methodName = target.suffix(lastDot + 1) and
96+
nsAndClass = target.prefix(lastDot) and
97+
secondLastDot = max(int i | nsAndClass.charAt(i) = ".") and
98+
namespace = nsAndClass.prefix(secondLastDot) and
99+
className = nsAndClass.substring(secondLastDot + 1, nsAndClass.length())
100+
)
101+
)
102+
}
103+
104+
/** Gets the enclosing method with extended metadata. */
105+
CilMethodExt getEnclosingMethodExt() { result = this.getEnclosingMethod() }
106+
}
107+
108+
/**
109+
* Holds if `caller` contains a call to a method identified by `(namespace, className, methodName)`.
110+
*/
111+
predicate hasCallTo(CilMethodExt caller, string namespace, string className, string methodName) {
112+
exists(CilCallExt call |
113+
call.getEnclosingMethodExt() = caller and
114+
call.targetsMethod(namespace, className, methodName)
115+
)
116+
}
117+
118+
/**
119+
* Holds if `caller` contains a call to a method with the given fully qualified target name.
120+
*/
121+
predicate hasCallToTarget(CilMethodExt caller, string targetFqn) {
122+
exists(CilCallExt call |
123+
call.getEnclosingMethodExt() = caller and
124+
call.getCallTargetFullyQualifiedName() = targetFqn
125+
)
126+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @name Call to vulnerable method
3+
* @description Calling a method with a known security vulnerability may expose the application
4+
* if the relevant dependency is not updated.
5+
* @kind problem
6+
* @problem.severity warning
7+
* @precision high
8+
* @id binary/vulnerable-calls
9+
*/
10+
11+
import VulnerableCalls
12+
13+
from VulnerableMethodCall call, string id
14+
where call.getVulnerabilityId() = id
15+
select call, "This call is potentially vulnerable to " + id
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Provides predicates for finding calls that transitively make a call to a
3+
* known-vulnerable method in CIL (C# IL) binaries.
4+
*/
5+
6+
private import binary
7+
import semmle.code.binary.cil.CilCallable
8+
9+
/**
10+
* Holds if any call identified by `(namespace, className, methodName)` should be flagged
11+
* as potentially vulnerable, for reasons explained by the advisory with the given `id`.
12+
*
13+
* This is an extensible predicate - values are provided via YAML data extensions.
14+
*/
15+
extensible predicate vulnerableCallModel(
16+
string namespace, string className, string methodName, string id
17+
);
18+
19+
/**
20+
* A method call that has been marked as vulnerable by a model.
21+
*/
22+
class VulnerableMethodCall extends CilCallExt {
23+
string vulnerabilityId;
24+
25+
VulnerableMethodCall() {
26+
exists(string namespace, string className, string methodName |
27+
vulnerableCallModel(namespace, className, methodName, vulnerabilityId) and
28+
this.targetsMethod(namespace, className, methodName)
29+
)
30+
}
31+
32+
/** Gets the vulnerability ID associated with this call. */
33+
string getVulnerabilityId() { result = vulnerabilityId }
34+
35+
/** Gets the enclosing method. */
36+
CilMethodExt getEnclosingVulnerableMethod() { result = this.getEnclosingMethodExt() }
37+
}
38+
39+
/**
40+
* Gets a call that a model has marked as vulnerable for the reason given by `id`.
41+
*/
42+
VulnerableMethodCall getAVulnerableCallFromModel(string id) { result.getVulnerabilityId() = id }
43+
44+
/**
45+
* Gets a method that directly contains a vulnerable call.
46+
*/
47+
CilMethodExt getADirectlyVulnerableMethod(string id) {
48+
result = getAVulnerableCallFromModel(id).getEnclosingVulnerableMethod()
49+
}
50+
51+
/**
52+
* Gets a method that transitively calls a vulnerable method.
53+
* This computes the transitive closure of the call graph.
54+
*/
55+
CilMethodExt getAVulnerableMethod(string id) {
56+
// Direct call to vulnerable method
57+
result = getADirectlyVulnerableMethod(id)
58+
or
59+
// Transitive: method calls another method that is vulnerable
60+
exists(CilCallExt call, CilMethodExt callee |
61+
call.getEnclosingMethodExt() = result and
62+
callee = getAVulnerableMethod(id) and
63+
call.getCallTargetFullyQualifiedName() = callee.getFullyQualifiedName()
64+
)
65+
}
66+
67+
/**
68+
* Gets a public method that transitively calls a vulnerable method.
69+
*/
70+
CilMethodExt getAPublicVulnerableMethod(string id) {
71+
result = getAVulnerableMethod(id) and
72+
result.isPublic()
73+
}
74+
75+
/**
76+
* Module for exporting vulnerable method information in a format suitable for
77+
* model generation or further analysis.
78+
*/
79+
module ExportedVulnerableCalls {
80+
/**
81+
* Holds if `(namespace, className, methodName)` identifies a method that
82+
* leads to a vulnerable call identified by `id`.
83+
*/
84+
predicate pathToVulnerableMethod(
85+
string namespace, string className, string methodName, string id
86+
) {
87+
exists(CilMethodExt m |
88+
m = getAVulnerableMethod(id) and
89+
m.hasFullyQualifiedName(namespace, className, methodName)
90+
)
91+
}
92+
93+
/**
94+
* Holds if `(namespace, className, methodName)` identifies a public method
95+
* that leads to a vulnerable call identified by `id`.
96+
*/
97+
predicate publicPathToVulnerableMethod(
98+
string namespace, string className, string methodName, string id
99+
) {
100+
exists(CilMethodExt m |
101+
m = getAPublicVulnerableMethod(id) and
102+
m.hasFullyQualifiedName(namespace, className, methodName)
103+
)
104+
}
105+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @name Methods that call vulnerable code
3+
* @description Lists all methods that transitively call a vulnerable method,
4+
* useful for generating models or understanding impact.
5+
* @kind problem
6+
* @problem.severity recommendation
7+
* @precision high
8+
* @id binary/vulnerable-calls-summarize
9+
*/
10+
11+
import VulnerableCalls
12+
13+
from CilMethodExt method, string id, string namespace, string className, string methodName
14+
where
15+
method = getAVulnerableMethod(id) and
16+
method.hasFullyQualifiedName(namespace, className, methodName)
17+
select method,
18+
"Method " + namespace + "." + className + "." + methodName +
19+
" transitively calls vulnerable code (" + id + ")"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
lockVersion: 1.0.0
3+
dependencies:
4+
codeql/controlflow:
5+
version: 2.0.21
6+
codeql/dataflow:
7+
version: 2.0.21
8+
codeql/ssa:
9+
version: 2.0.13
10+
codeql/typetracking:
11+
version: 2.0.21
12+
codeql/util:
13+
version: 2.0.24
14+
compiled: false
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Model files go here
2+
# Example format:
3+
#
4+
# extensions:
5+
# - addsTo:
6+
# pack: binary/vulnerable-calls
7+
# extensible: vulnerableCallModel
8+
# data:
9+
# - ["System.Net.Http", "HttpClient", "GetAsync", "CVE-2024-XXXX"]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extensions:
2+
- addsTo:
3+
pack: binary/vulnerable-calls
4+
extensible: vulnerableCallModel
5+
data:
6+
- ["System", "Console", "WriteLine", "TEST-VULN-001"]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name: binary/vulnerable-calls
2+
version: 0.0.1
3+
dependencies:
4+
microsoft/binary-all: "*"
5+
extractor: cil
6+
dataExtensions:
7+
- models/**/*.yml

0 commit comments

Comments
 (0)