Skip to content

Commit 628bab9

Browse files
committed
Crypto: Modify BadMacOrderMacOnEncryptPlaintext to be a path query that traces through any intermediate encrypt or mac to the final encrypt or mac.
1 parent ff7840d commit 628bab9

File tree

3 files changed

+140
-26
lines changed

3 files changed

+140
-26
lines changed
Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,119 @@
11
/**
2-
* @name Bad MAC order: MAC on an encrypt plaintext
2+
* @name Bad MAC order: Mac and Encryption share the same plaintext
33
* @description MAC should be on a cipher, not a raw message
44
* @id java/quantum/bad-mac-order-encrypt-plaintext-also-in-mac
5-
* @kind problem
5+
* @kind path-problem
66
* @problem.severity error
77
* @tags quantum
88
* experimental
99
*/
1010

1111
import java
1212
import experimental.quantum.Language
13+
import codeql.util.Option
1314

14-
// NOTE: I must look for a common data flow node rather than
15-
// starting from a message source, since the message source
16-
// might not be known.
17-
// TODO: can we approximate a message source better?
18-
module CommonDataFlowNodeConfig implements DataFlow::ConfigSig {
19-
predicate isSource(DataFlow::Node source) {
20-
exists(source.asParameter())
15+
module ArgToSinkConfig implements DataFlow::ConfigSig {
16+
predicate isSource(DataFlow::Node source) { exists(Call c | c.getAnArgument() = source.asExpr()) }
17+
18+
predicate isSink(DataFlow::Node sink) { targetSinks(sink) }
19+
20+
// Don't go in to a known out node, this will prevent the plaintext
21+
// from tracing out of cipher operations for example, we just want to trace
22+
// the plaintext to uses.
23+
// NOTE: we are not using a barrier out on input nodes, because
24+
// that would remove 'use-use' flows, which we need
25+
predicate isBarrierIn(DataFlow::Node node) {
26+
node = any(Crypto::FlowAwareElement element).getOutputNode()
27+
}
28+
29+
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
30+
node1.(AdditionalFlowInputStep).getOutput() = node2
2131
or
32+
exists(MethodCall m |
33+
m.getMethod().hasQualifiedName("java.lang", "String", "getBytes") and
34+
node1.asExpr() = m.getQualifier() and
35+
node2.asExpr() = m
36+
)
37+
}
38+
}
39+
40+
module ArgToSinkFlow = TaintTracking::Global<ArgToSinkConfig>;
41+
42+
/**
43+
* Target sinks for this query are either encryption operations or mac operation message inputs
44+
*/
45+
predicate targetSinks(DataFlow::Node n) {
46+
exists(Crypto::CipherOperationNode cipherOp |
47+
cipherOp.getKeyOperationSubtype() = Crypto::TEncryptMode() and
48+
cipherOp.getAnInputArtifact().asElement() = n.asExpr()
49+
)
50+
or
51+
exists(Crypto::MacOperationNode macOp | macOp.getAnInputArtifact().asElement() = n.asExpr())
52+
}
53+
54+
/**
55+
* An argument of a target sink or a parent call whose parameter flows to a target sink
56+
*/
57+
class InterimArg extends DataFlow::Node {
58+
DataFlow::Node targetSink;
59+
60+
InterimArg() {
61+
targetSinks(targetSink) and
62+
(
63+
this = targetSink
64+
or
65+
ArgToSinkFlow::flow(this, targetSink) and
66+
this.getEnclosingCallable().calls+(targetSink.getEnclosingCallable())
67+
)
68+
}
69+
70+
DataFlow::Node getTargetSink() { result = targetSink }
71+
}
72+
73+
/**
74+
* A wrapper class to represent a target argument dataflow node.
75+
*/
76+
class TargetArg extends DataFlow::Node {
77+
TargetArg() { targetSinks(this) }
78+
79+
predicate isCipher() {
80+
exists(Crypto::CipherOperationNode cipherOp |
81+
cipherOp.getKeyOperationSubtype() = Crypto::TEncryptMode() and
82+
cipherOp.getAnInputArtifact().asElement() = this.asExpr()
83+
)
84+
}
85+
86+
predicate isMac() {
87+
exists(Crypto::MacOperationNode macOp | macOp.getAnInputArtifact().asElement() = this.asExpr())
88+
}
89+
}
90+
91+
module PlaintextUseAsMacAndCipherInputConfig implements DataFlow::StateConfigSig {
92+
class FlowState = Option<TargetArg>::Option;
93+
94+
// TODO: can we approximate a message source better?
95+
predicate isSource(DataFlow::Node source, FlowState state) {
96+
// TODO: can we find the 'closest' parameter to the sinks?
97+
// i.e., use a generic source if we have it, but also isolate the
98+
// lowest level in the flow to the closest parameter node in the call graph?
2299
exists(Crypto::GenericSourceNode other |
23100
other.asElement() = CryptoInput::dfn_to_element(source)
101+
) and
102+
state.isNone()
103+
}
104+
105+
predicate isSink(DataFlow::Node sink, FlowState state) {
106+
sink instanceof TargetArg and
107+
(
108+
sink.(TargetArg).isMac() and state.asSome().isCipher()
109+
or
110+
sink.(TargetArg).isCipher() and state.asSome().isMac()
24111
)
25112
}
26113

27-
predicate isSink(DataFlow::Node sink) {
28-
sink = any(Crypto::FlowAwareElement other).getInputNode()
114+
predicate isBarrierOut(DataFlow::Node node, FlowState state) {
115+
// Stop at the first sink for now
116+
isSink(node, state)
29117
}
30118

31119
// Don't go in to a known out node, this will prevent the plaintext
@@ -46,19 +134,32 @@ module CommonDataFlowNodeConfig implements DataFlow::ConfigSig {
46134
node2.asExpr() = m
47135
)
48136
}
137+
138+
predicate isAdditionalFlowStep(
139+
DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2
140+
) {
141+
(exists(state1.asSome()) or state1.isNone()) and
142+
targetSinks(node1) and
143+
node1 instanceof TargetArg and
144+
//use-use flow, either flow directly from the node1 use
145+
//or find a parent call in the call in the call stack
146+
//and continue flow from that parameter
147+
node2.(InterimArg).getTargetSink() = node1 and
148+
state2.asSome() = node1
149+
}
49150
}
50151

51-
module CommonDataFlowNodeFlow = TaintTracking::Global<CommonDataFlowNodeConfig>;
152+
module PlaintextUseAsMacAndCipherInputFlow =
153+
TaintTracking::GlobalWithState<PlaintextUseAsMacAndCipherInputConfig>;
52154

53-
from DataFlow::Node src, DataFlow::Node sink1, DataFlow::Node sink2
155+
import PlaintextUseAsMacAndCipherInputFlow::PathGraph
156+
157+
from
158+
PlaintextUseAsMacAndCipherInputFlow::PathNode src,
159+
PlaintextUseAsMacAndCipherInputFlow::PathNode sink, InterimArg arg
54160
where
55-
not src.asExpr() instanceof NullLiteral and
56-
CommonDataFlowNodeFlow::flow(src, sink1) and
57-
CommonDataFlowNodeFlow::flow(src, sink2) and
58-
exists(Crypto::CipherOperationNode cipherOp |
59-
cipherOp.getKeyOperationSubtype() = Crypto::TEncryptMode() and
60-
cipherOp.getAnInputArtifact().asElement() = sink1.asExpr()
61-
) and
62-
exists(Crypto::MacOperationNode macOp | macOp.getAnInputArtifact().asElement() = sink2.asExpr())
63-
select src, "Message used for encryption operation at $@, also used for MAC at $@.", sink1,
64-
sink1.toString(), sink2, sink2.toString()
161+
PlaintextUseAsMacAndCipherInputFlow::flowPath(src, sink) and
162+
arg = sink.getState().asSome()
163+
select sink, src, sink,
164+
"Source is used as plaintext to MAC and encryption operation. Indicates possible misuse of MAC. Path shows plaintext to final use through intermediate mac or encryption operation here $@",
165+
arg.asExpr(), arg.asExpr().toString()
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,14 @@
1-
| BadMacUse.java:67:82:67:97 | plaintext | Message used for encryption operation at $@, also used for MAC at $@. | BadMacUse.java:80:44:80:52 | plaintext | plaintext | BadMacUse.java:75:42:75:50 | plaintext | plaintext |
1+
#select
2+
| BadMacUse.java:80:44:80:52 | plaintext | BadMacUse.java:67:82:67:97 | plaintext : byte[] | BadMacUse.java:80:44:80:52 | plaintext | Source is used as plaintext to MAC and encryption operation. Indicates possible misuse of MAC. Path shows plaintext to final use through intermediate mac or encryption operation here $@ | BadMacUse.java:75:42:75:50 | plaintext | plaintext |
3+
edges
4+
| BadMacUse.java:67:82:67:97 | plaintext : byte[] | BadMacUse.java:75:42:75:50 | plaintext : byte[] | provenance | |
5+
| BadMacUse.java:75:42:75:50 | plaintext : byte[] | BadMacUse.java:75:42:75:50 | plaintext : byte[] | provenance | Config |
6+
| BadMacUse.java:75:42:75:50 | plaintext : byte[] | BadMacUse.java:80:44:80:52 | plaintext | provenance | |
7+
nodes
8+
| BadMacUse.java:67:82:67:97 | plaintext : byte[] | semmle.label | plaintext : byte[] |
9+
| BadMacUse.java:75:42:75:50 | plaintext : byte[] | semmle.label | plaintext : byte[] |
10+
| BadMacUse.java:75:42:75:50 | plaintext : byte[] | semmle.label | plaintext : byte[] |
11+
| BadMacUse.java:80:44:80:52 | plaintext | semmle.label | plaintext |
12+
subpaths
13+
testFailures
14+
| BadMacUse.java:54:56:54:66 | // $Source | Missing result: Source |

java/ql/test/experimental/query-tests/quantum/examples/BadMacUse/BadMacUse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public void BadDecryptThenMacOnPlaintextVerify(byte[] encryptionKeyBytes, byte[]
6464
}
6565
}
6666

67-
public void BadMacOnPlaintext(byte[] encryptionKeyBytes, byte[] macKeyBytes, byte[] plaintext) throws Exception {// $Alert[java/quantum/bad-mac-order-encrypt-plaintext-also-in-mac]
67+
public void BadMacOnPlaintext(byte[] encryptionKeyBytes, byte[] macKeyBytes, byte[] plaintext) throws Exception {// $Source
6868
// Create keys directly from provided byte arrays
6969
SecretKey encryptionKey = new SecretKeySpec(encryptionKeyBytes, "AES");
7070
SecretKey macKey = new SecretKeySpec(macKeyBytes, "HmacSHA256");
@@ -77,7 +77,7 @@ public void BadMacOnPlaintext(byte[] encryptionKeyBytes, byte[] macKeyBytes, byt
7777
// Encrypt the plaintext
7878
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
7979
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new SecureRandom());
80-
byte[] ciphertext = cipher.doFinal(plaintext);
80+
byte[] ciphertext = cipher.doFinal(plaintext); // $Alert[java/quantum/bad-mac-order-encrypt-plaintext-also-in-mac]
8181

8282
// Concatenate ciphertext and MAC
8383
byte[] output = new byte[ciphertext.length + computedMac.length];

0 commit comments

Comments
 (0)