Nico Waisman
Open source has won and is here to stay, but it comes with challenges. Open Source security is one of them that we face as an industry. We all consume it but what about its code quality, security practices, … Over the last 3 months, Github's Semmle Security Research Team has been triaging all open source CVEs and engaging on a subset of those performing variant analysis trying to uncover what it was missed. During this talk we will present some of these cases where we used QL to perform variant analysis, in addition to some others where we performed the full research (seed vulnerability and variant analysis) such as u-boot.
BlueHat Seattle 2019 || Open Source Security, vulnerabilities never come alone
1.
2. Nico Waisman - Principal Security Engineer
Open Source Security:
vulnerabilities never
come alone
3. Principal Security Engineer @ Github
─ Helping manage the team
─ Full time Security Research
Mission:
─ Help secure the Open Source landscape
─ Finding bugs!
─ Focus on C/C++ and Golang
─ Love for bizarre corner cases
3
About me - @nicowaisman
5. But it comes with challenges:
─ Code quality
─ Supply chain
─ Common development
best practices:
─ Code review
─ Testing
─ ...
─ Security: CVE tagging
5
Open source has won… everywhere!
https://resources.whitesourcesoftware.com/blog-whitesource/git-much-the-top-10-companies-contributing-to-open-source
6. 6
CVE system in OSS, working?
https://www.openwall.com/lists/oss-security/2019/06/15/2
12. ─ Continuous fuzzing service
─ Leverages Google’s large capacity
─ Reduces the gap between development
and vulnerability discovery
─ Integrate and get notified about vulnerabilities
─ Provide a Dockerfile
─ Provide a function to interact with the
software
─ Free for open source
12
Google’s OSS-Fuzz
Impact:
─ 200+ OSS projects opted-in
─ 9k+ vulnerabilities uncovered
13. ─ Vulnerability bounties on relevant software
─ Sponsored by companies
13
Internet Bug Bounty
Impact:
─ Covers 19 of the largest OSS
─ $700k+ rewards so far:
ImageTragick ($7.5k),
Heartbleed ($15k), Shellshock
($20k), ...
15. ─ Dependabot and Whitesource partnership
─ Automatically alert maintainers on risky
dependencies and vulnerabilities
─ Maintainer security advisories
─ No more, where do I send this
vulnerability? Is this mailing list public?
─ Security policy
─ CNA
─ Donations
15
GitHub
Impact:
─ Make security easy for non security
developers
─ Provide a way to incentivize developers
you depend on
Business continuity plan?
16. Securing software, together
from AddExpr a, Variable v, RelationalOperation cmp
where a.getAnOperand() = v.getAnAccess()
and cmp.getAnOperand() = a
and cmp.getAnOperand() = v.getAnAccess()
and forall(Expr op | op = a.getAnOperand() |
op.getType().getSize() < 4)
and not a.getExplicitlyConverted().getType().getSize() <
4
select cmp, "Bad overflow check"
Sample QL query identifying common mistakes made checking for integer overflows
QL is an object oriented query language for exploring code as data
Security engineers use QL to
- Explore code iteratively
- Map attack surfaces
- Automate variant
analysis
QL - Automating variant analysis
QL ensures security knowledge is codified, readable and executable
20. Securing software, together 20
July 21
Linus Torvalds
Provides Fix
July 18, 2019
Tavis Ormandy
GoogleProject Zero
Finds Valid Vector
4 Variants
Vulnerabilities never walk alone (I)
Linux Kernel
Buffer Overflow
July 18, 2019
Nico Waisman
Github
Writes Query
Nico Waisman
Github
Weekly CVE triage
July 18, 2019
21. Essentially we are looking for:
while (condition) {
[...]
array[x] = y;
[...]
}
21
Array write inside a loop query
import cpp
from Variable buffer, Expr bufferWriteBase, ArrayExpr bufferWrite,
Assignment a, Loop loop
where
buffer.getType() instanceof ArrayType and
bufferWriteBase = buffer.getAnAccess() and
bufferWriteBase = bufferWrite.getArrayBase() and
bufferWrite = a.getLValue() and
loop = a.getEnclosingStmt().getParent*()
select a
25. ─ Open source bootloader
─ Used by
─ Kindle devices
─ ChromeOS ARM devices
─ IoT
─ Supports verifiable boot
─ Any vulnerability before the signature
check is a potential jailbreak
─ Filesystems should then be considered
untrusted
─ …...Not to mention any networking :)
25
What is U-Boot?
26. 26
How we used variant analysis Vulnerability counter: 0
Find the seed
Write QL
Query
Refine QL
Query
Triage
27. Vulnerability:
─ 2x memcpy() size
argument is attacker
controlled
─ size comes from the
NFS packet
─ No size constraints
27
The seed vulnerability Vulnerability counter: 2
28. Find all memcpy callers
─ 596 instances
Find usage of rpc_pkt.u.reply
─ 191 instances
28
Query initial steps
import cpp
from FunctionCall call
where call.getTarget().getName() = "memcpy"
select call
Vulnerability counter: 2
import cpp
from FieldAccess access, Field f
where f = access.getTarget() and f.hasName("reply")
select access
29. Find all memcpy callers
─ 596 instances
Find usage of rpc_pkt.u.reply
─ 191 instances
29
Query initial steps Vulnerability counter: 2
import cpp
from FieldAccess access, Field f
where f = access.getTarget() and f.hasName("reply")
select access
import cpp
from
FunctionCall call
where
call.getTarget().getName() = "memcpy"
select
call
30. Find all memcpy callers
─ 596 instances
Find usage of rpc_pkt.u.reply
─ 191 instances
30
Query initial steps
import cpp
from FunctionCall call
where call.getTarget().getName() = "memcpy"
select call
Vulnerability counter: 2
import cpp
from FieldAccess access, Field f
where f = access.getTarget() and
f.hasName("reply")
select access
31. 31
Given a (problem-specific) set of sources
and sinks, is there a path
in the data flow graph from some source
to some sink?
32. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
32
Query refining process
import cpp
import semmle.code.cpp.dataflow.TaintTracking
class NetworkToMemFuncLength extends TaintTracking::Configuration {
NetworkToMemFuncLength() { this = "NetworkToMemFuncLength" }
override predicate isSource(DataFlow::Node source) {
exists (FieldAccess access, Field f |
source.asExpr() = access and f = access.getTarget() and
f.hasName("reply"))
}
override predicate isSink(DataFlow::Node sink) {
exists (FunctionCall fc, Expr argument |
sink.asExpr() = argument and (argument = fc.getArgument(2) and
fc.getTarget().hasQualifiedName("memcpy"))
)
}
}
from Expr socket_buffer, Expr sizeArg, NetworkToMemFuncLength config
where config.hasFlow(DataFlow::exprNode(socket_buffer),
DataFlow::exprNode(sizeArg))
select sizeArg
Vulnerability counter: 2
33. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
33
Query refining process Vulnerability counter: 2
override predicate isSource(DataFlow::Node
source) {
exists (
FieldAccess access, Field f |
source.asExpr() = access and
f = access.getTarget() and
f.hasName("reply"))
}
34. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
34
Query refining process Vulnerability counter: 2
override predicate isSource(DataFlow::Node
source) {
exists (
FieldAccess access, Field f |
source.asExpr() = access and
f = access.getTarget() and
f.hasName("reply"))
}
35. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
35
Query refining process Vulnerability counter: 2
override predicate isSource(DataFlow::Node
source) {
exists (
FieldAccess access, Field f |
source.asExpr() = access and
f = access.getTarget() and
f.hasName("reply"))
}
36. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
36
Query refining process Vulnerability counter: 2
override predicate isSource(DataFlow::Node
source) {
exists (
FieldAccess access, Field f |
source.asExpr() = access and
f = access.getTarget() and
f.hasName("reply"))
}
37. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
37
Query refining process Vulnerability counter: 2
override predicate isSource(DataFlow::Node
source) {
exists (
FieldAccess access, Field f |
source.asExpr() = access and
f = access.getTarget() and
f.hasName("reply"))
}
38. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
38
Query refining process
override predicate isSink(DataFlow::Node
sink) {
exists (
FunctionCall fc, Expr argument |
sink.asExpr() = argument and
(argument = fc.getArgument(2) and
fc.getTarget().hasQualifiedName("memcpy"
))
)
}
Vulnerability counter: 2
39. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
39
Query refining process
override predicate isSink(DataFlow::Node
sink) {
exists (
FunctionCall fc, Expr argument |
sink.asExpr() = argument and
(argument = fc.getArgument(2) and
fc.getTarget().hasQualifiedName("memcpy"
))
)
}
Vulnerability counter: 2
40. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
40
Query refining process
override predicate isSink(DataFlow::Node
sink) {
exists (
FunctionCall fc, Expr argument |
sink.asExpr() = argument and
(argument = fc.getArgument(2) and
fc.getTarget().hasQualifiedName("memcpy"
))
)
}
Vulnerability counter: 2
41. The first real query
─ Data Flow analysis
─ Source: nfs data packet
read from the socket
─ Sink: memcpy()
Results:
─ 4 instances
─ We found the 2 original
seed vulnerabilities
41
Query refining process Vulnerability counter: 2
from
Expr socket_buffer, Expr sizeArg,
NetworkToMemFuncLength config
where
config.hasFlow(DataFlow::exprNode(socket_bu
ffer), DataFlow::exprNode(sizeArg))
select
sizeArg
42. Looks like they are doing the correct by checking the length
Until you discover that filefh3_length is a signed integer!!!
Vulnerability Counter += 1
42
Findings triage Vulnerability counter: 3
43. Same story:
─ rlen comes directly from the NFS
packet
─ rlen is consumed as the size for
the memcpy() operation
─ No checks!
memcpy() happens inside store_block()
in two different locations
Vulnerability Counter += 2
43
Findings triage Vulnerability counter: 5
45. Let’s think bigger beyond NFS:
─ Data Flow analysis
─ Source: ntohl(), ntohs() and
friends
─ Sink: memcpy()
Results:
─ 8 instances
─ We still found the 2 original
seed vulnerabilities,
previously discussed
variants and more!
45
Query expansion
import cpp
import semmle.code.cpp.dataflow.TaintTracking
class NetworkByteOrderTranslation extends Expr {
NetworkByteOrderTranslation() {
this = any(MacroInvocation mi |
mi.getOutermostMacroAccess().getMacroName().regexpMatch("(?i)(^|.*_)n
toh(l|ll|s)")
).getExpr()
}
}
[...]
override predicate isSource(DataFlow::Node source) {
exists (FieldAccess access, Field f |
source.asExpr() instanceof NetworkByteOrderTranslation
}
[...]
from Expr socket_buffer, Expr sizeArg, NetworkToMemFuncLength config
where config.hasFlow(DataFlow::exprNode(socket_buffer),
DataFlow::exprNode(sizeArg))
select sizeArg
Vulnerability counter: 5
46. Let’s think bigger beyond NFS:
─ Data Flow analysis
─ Source: ntohl(), ntohs() and
friends
─ Sink: memcpy()
Results:
─ 8 instances
─ We still found the 2 original
seed vulnerabilities,
previously discussed
variants and more!
46
Query expansion Vulnerability counter: 5
class NetworkByteOrderTranslation extends Expr {
NetworkByteOrderTranslation() {
this = any(
MacroInvocation mi |
mi.getOutermostMacroAccess().getMacroName().regexpMatch(
"(?i)(^|.*_)ntoh(l|ll|s)")
).getExpr()
}
}
override predicate isSource(DataFlow::Node source) {
exists (FieldAccess access, Field f |
source.asExpr() instanceof NetworkByteOrderTranslation
}
47. Let’s think bigger beyond NFS:
─ Data Flow analysis
─ Source: ntohl(), ntohs() and
friends
─ Sink: memcpy()
Results:
─ 8 instances
─ We still found the 2 original
seed vulnerabilities,
previously discussed
variants and more!
47
Query expansion Vulnerability counter: 5
class NetworkByteOrderTranslation extends Expr {
NetworkByteOrderTranslation() {
this = any(
MacroInvocation mi |
mi.getOutermostMacroAccess().getMacroName().regexpMatch(
"(?i)(^|.*_)ntoh(l|ll|s)")
).getExpr()
}
}
override predicate isSource(DataFlow::Node source) {
exists (FieldAccess access, Field f |
source.asExpr() instanceof NetworkByteOrderTranslation
}
48. TCP/IP stack
2 integer underflows with no bounds checking later ending up in memcpy()
Vulnerability Counter += 2
48
Findings triage Vulnerability counter: 7
49. Plain stack overflow
Also happens in 4 other different functions
Vulnerability Counter += 5
49
Other vulnerabilities Vulnerability counter: 12
50. Clearly, someone is trying to prevent the overflow
But, forgot to check the lower bound… would be a READ OOB
Vulnerability Counter += 1
50
Other vulnerabilities Vulnerability counter: 13
55. We start with cfg80211_find_ie:
─ Data Flow analysis
─ Source: cfg80211_find_ie
─ Sink: memcpy() size
Results:
─ 13 instances
─ No bugs. Code looks
buggy, but there were
sanitized
─ What other function deal
with IE?
55
Dataflow query cfg80211_find_ie
class GetIE extends TaintTracking::Configuration {
GetIE() { this="cfg80211_find_ie" }
override predicate isSource(DataFlow::Node source) {
exists( Function sl |
source.asExpr().(FunctionCall).getTarget() =
sl
and
sl.hasQualifiedName("cfg80211_find_ie")
)
}
override predicate isSink(DataFlow::Node sink) {
exists(FunctionCall fc |
sink.asExpr() = fc.getArgument(2)
and
fc.getTarget().hasQualifiedName("memcpy")
)
}
}
Vulnerability counter: 0
56. We start with cfg80211_find_ie:
─ Data Flow analysis
─ Source: cfg80211_find_ie
─ Sink: memcpy() size
Results:
─ 13 instances
─ No bugs. Code looks
buggy, but there were
sanitized
─ What other function deal
with IE?
56
Dataflow query cfg80211_find_ie
override predicate isSource(DataFlow::Node
source) {
exists( Function sl |
source.asExpr().(FunctionCall).getTarget() = sl
and
sl.hasQualifiedName("cfg80211_find_ie")
)
Vulnerability counter: 0
57. We start with cfg80211_find_ie:
─ Data Flow analysis
─ Source: cfg80211_find_ie
─ Sink: memcpy() size
Results:
─ 13 instances
─ No bugs. Code looks
buggy, but there were
sanitized
─ What other function deal
with IE?
57
Dataflow query cfg80211_find_ie
override predicate isSink(DataFlow::Node sink) {
exists(FunctionCall fc |
sink.asExpr() = fc.getArgument(2)
and
fc.getTarget().hasQualifiedName("memcpy")
)
}
}
Vulnerability counter: 0
58. A new IE based function:
─ Data Flow analysis
─ Source:
ieee80211_bss_get_ie
─ Sink: memcpy() size
Results:
─ 10 instances
─ Most of the match looks
promising, but a big chunk
where sanitized.
Extending to others IE functions
58
class GetIE extends TaintTracking::Configuration {
GetIE() { this="ieee80211_bss_get_ie" }
override predicate isSource(DataFlow::Node source) {
exists( Function sl |
source.asExpr().(FunctionCall).getTarget() =
sl
and
sl.hasQualifiedName("ieee80211_bss_get_ie")
)
}
override predicate isSink(DataFlow::Node sink) {
exists(FunctionCall fc |
sink.asExpr() = fc.getArgument(2)
and
fc.getTarget().hasQualifiedName("memcpy")
)
}
}
Vulnerability counter: 0
59. 59
CVE variant: CVE-2019-16746 Vulnerability counter: 1
● cw1200 wireless driver
● Remotely exploitable through a beacon
60. 60
CVE variant: CVE-2019-17133 Vulnerability counter: 2
● cfg80211 wext compat for managed mode.
● Remotely exploitable through a beacon
61. Securing software, together 61
Linux Team
Provides Fix for
nl80211/cfg80211
August 30, 2019
Nico Waisman
Github
Report 2 bugs
4 Variants
Vulnerabilities never walk alone (II)
Linux Kernel
Buffer Overflow
August 29, 2019
Nico Waisman
Github
Writes Query
huangwen
ADLab
CVE-2019-1481[4-6]
August 28, 2019 Oct 4, 2019
Linux Team
Provides Fix for
mac80211/wext-sme
Sep 20, 2019
62. 62
Lot of work left!
● Find more source
● Improve the sinks!
Use QL to explore your code, beyond variant
analysis.