Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
215 lines (188 sloc) 9.73 KB
---
- DESCRIPTION: |-
Look for a for statement with an increment based on user input. Based on this bug:
https://anonsvn.wireshark.org/viewvc/trunk/epan/dissectors/packet-assa_r3.c?r1=51196&r2=51195&pathrev=51196
QUERY: |-
getNodesWithType("ForStatement")
.children()
.filter { it.type == "AssignmentExpr" }
.filter { it.rval().code.toList()[0].matches(".*tvb_get_.*") }
POSITIVE_TESTS:
- |-
main() {
for (i = 0; i < len; i += tvb_get_guint8(tvb, i)) {
}
}
NEGATIVE_TESTS:
- DESCRIPTION: |-
More generically, Wireshark dissectors normally update their position in a packet/stream in
a variable named offset as they process. There are a number of previous infinite loop DoS
vulnerabilities based on the offset being incremented in a loop by user-controlled data set
to 0 or being able to wrap around the offset variable in a loop, for example:
https://code.wireshark.org/review/#/c/5338/2/epan/dissectors/packet-tn5250.c
Check a value that comes from tvb_get_* is later added to the offset variable in a loop.
Limitations:
+ Assumes an assignment, does not work for offset += tvb_...
QUERY: |-
getCallsTo("tvb_get_*")
.statements()
.filter { it.defines().count() > 0 }
.sideEffect { output_var_name = it.defines().toList()[-1].code }
.out("REACHES")
.match { it.type == "AssignmentExpr" }
.and(
_().lval().match { it.code.matches(".*[oO]ff.*") },
_().rval().filter { it.type != "CallExpression" }.match { it.code == output_var_name })
.dedup()
.filter { isInLoop(it) }
POSITIVE_TESTS:
- |-
tvb_get_ntohl() {} // Declaration required to be able to search for functions by name in the index
main() {
while(1) {
guint32 len = tvb_get_ntohl(tvb, offset);
offset += len;
}
}
NEGATIVE_TESTS:
- |-
tvb_get_ntohl() {} // Declaration required to be able to search for functions by name in the index
main() {
guint32 len = tvb_get_ntohl(tvb, offset);
offset += len;
}
- DESCRIPTION: |-
The above is a good start, but it turns out that many core Wireshark functions check bounds
for the offset so there is an unacceptable number of false-positives. There are two types
of bugs the above query could find: 1) infinite loops caused by loops updating the offset
using only user supplied input [e.g. no constants are added], or 2) infinite loops caused
by integer overflows to the offset variable, causing the loop to start processing at the
beginning of the last iteration. This query looks for the latter.
There is an 'unsanitized' joern step that allows you to pick a statement in the CFG and
find all paths in the CFG to that statement where user input has not been sanitized. We
would like to do the opposite, start from a call to tvb_get_*, end at a statement that
adds that value to the offset, and make sure the argument has not been passed to a
bounds-checking function in between. We can do this by using the 'cfgPaths' step (which is
used by 'unsanitized' step.) This allows us specify a source and a destination statement,
along with a closure expressing what 'sanitizing' statements look like, and receive any
paths from the source to the destination where a given set of sanitizing statements are
avoided.
Since we are interested in integer overflows, we only care to target functions that return
32-bit or 64-bit integers. Afterwards we use slightly more complex logic to find instances
where that tainted user input is added to an offset variable, and then perform the cfgPaths
sanitization search.
This query found the following bugs:
+ https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=11023
+ https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=11024
+ https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=11037
Limitations:
+ Assumes an assignment, fails for offset += tvb_...
+ Assumes a call to proto_tree_add_item/etc sanitizes, but it might miss cases where the
tainted value is added to something [e.g. proto_tree_add_item(tained + 4) allowing an
integer overflow to bypass the bounds checking]
+ The tainted user data might be passed as an argument to a sanitizing functions so that
it does not cause a bound check, we should check specific parameters.
QUERY: |-
getFunctionsByName("tvb_get_*").as('func')
.out("IS_FUNCTION_OF_AST").out.filter { it.type == "ReturnType" }
.filter { it.code.matches(".*int(32|64).*") }
.back('func')
.transform { getCallsTo(it.name) }.scatter()
.sideEffect { src = it }
.statements()
.filter { it.defines().count() > 0 }
.sideEffect { output_var_name = it.defines().code.toList()[0].replace("*", "") }
.out("REACHES")
.match { it.type == "AssignmentExpr" }
.filter { it.rval().toList()[0].code.matches(".*$output_var_name.*") }
.filter { it.rval().toList()[0].type != 'CallExpression' }
// The following is to get around null accesses stemming from:
// https://github.com/fabsx00/joern/issues/49
.filter { it.parents().toList()[0].type != "IdentifierDecl" }
.filter { lval = it.lval().toList()[0]; lval.code == 'offset' || (src.ithArguments("1").toList()[0].code != 'offset' && lval.code.matches(".*[oO]ff.*")) }
.dedup()
.filter { isInLoop(it) }
.sideEffect { dst = it }
.transform { cfgPaths(output_var_name, { cur, sym ->
cur._().filter { it.uses().code.toList().contains(output_var_name) }.or(
_().codeContains('.*proto_tree_add_(text|item).*'),
_().codeContains('.*tvb_get_(str|ptr).*'),
_().codeContains('.*tvb_new_subset.*'),
_().codeContains('.*tlv_length_remaining.*'))
}, src.statements().toList()[0], dst.statements().toList()[0]) }
.scatter().transform { it.toList()[0] }
.dedup()
POSITIVE_TESTS:
- |-
guint32 tvb_get_ntohl() {} // Declaration required to be able to search for functions by name in the index
main() {
guint32 len;
while(1) {
len = tvb_get_ntohl(tvb, offset);
offset += len;
}
}
NEGATIVE_TESTS:
- |-
guint32 tvb_get_ntohl() {} // Declaration required to be able to search for functions by name in the index
main() {
guint32 len;
while(1) {
len = tvb_get_ntohl(tvb, offset);
proto_tree_add_text(len); // Sanitizer!
offset += len;
}
}
- DESCRIPTION: |-
The above query is good for finding cases where an infinite loop is possible by
overflowing the offset counter and causing the processing to start-back at the original
point; however, it is also possible that we could cause an infinite loop if we could cause
the offset to only be incremented by zero. This means looking for loops where the offset
is not incremented by fixed amounts and only by used controlled data.
This query requires a modification to cfgPaths, hence there are no unit tests because they
would fail with modifications :/ The modification is to allow specifying the same source and
destination node, e.g. making sure that we find no expressions that modify the offset
variable in a way that might force it to increment in an entire loop iteration.
Found https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=11036
Requires cfgPaths modification:
--- a/joern/joernsteps/taintTracking/dataflow.groovy
+++ b/joern/joernsteps/taintTracking/dataflow.groovy
@@ -127,7 +127,7 @@ Object.metaClass._cfgPaths = {symbol, sanitizer, curNode, dst, visited, path ->
}
// return path when destination has been reached
- if(curNode == dst){
+ if (curNode == dst && path != []) {
Limitations:
+ We only find the infinite loop case where only a single user-controlled zero variable is
added to the offset; however, there could be multiple such zero-value variables.
QUERY: |-
getFunctionsByName("tvb_get_ntohl").transform { getCallsTo(it.name) }.scatter()
.sideEffect { offset_var_regex = (it.ithArguments("1").toList()[0].code == 'offset' ? 'offset' : "[oO]ff") }
.sideEffect { src = it }
.statements()
.filter { it.defines().count() > 0 }
.sideEffect { output_var_name = it.defines().code.toList()[0].replace("*", "") }
.out("REACHES")
.match { it.type == "AssignmentExpr" }
.filter { it.rval().toList()[0].code.matches(".*$output_var_name.*") }
.filter { it.rval().toList()[0].type != 'CallExpression' }
.filter { it.rval().toList()[0].type != 'AdditiveExpression' }
.filter { it.parents().toList()[0].type != "IdentifierDecl" }
.filter { lval = it.lval().toList()[0]; lval.code == 'offset' || (src.ithArguments("1").toList()[0].code != 'offset' && lval.code.matches(".*[oO]ff.*")) }
.dedup()
.filter { isInLoop(it) }
.transform { cfgPaths('FAKEFAKE', { cur, sym ->
//println cur._().code.toList();
//println cur._().match { it.type == "AssignmentExpr" }.toList();
cur._().or(
_().match { it.type == "AssignmentExpr" }.filter { it.lval().toList().size > 0 }
.filter { it.lval().toList()[0].code.matches(".*$offset_var_regex.*") }
.filter { !it.rval().toList()[0].code.matches(".*$output_var_name.*") },
_().match { it.type == "IncDecOp" && it.lval().toList().size > 0 }
.filter { it.lval.toList()[0].code.matches(".*$offset_var_regex.*") },
_().isCheck(".*$output_var_name .*"))
}, src.statements().toList()[0], src.statements().toList()[0]) }
.scatter().transform { it.toList()[0] }
.dedup()
POSITIVE_TESTS:
NEGATIVE_TESTS: