Investigate a C Application

You can use Ocular to investigate your C language applications. This tutorial illustrates how, by using the example of CVE-2016-6480 Linux Kernel. The vulnerability is a race condition that exists in the Linux Kernel version 4.7, in the ioctl_send_fib in drivers/scsi/aacraid/commctrl.c function.

This article will should you how to use Ocular to investigate your applications written in C. More specifically, you will see how Ocular finds the presence of CVE-2016-6480 Linux Kernel, a race condition in the ioctl_send_fib function. This function can be found in drivers/scsi/aacraid/commctrl.c in Linux kernels 4.7 (or earlier).

Prerequisite

Before proceeding, please download the Linux kernel containing this vulnerability:

git clone https://github.com/torvalds/linux
cd linux
git checkout v4.7

Step 1: Create the Code Property Graph (CPG)

Create a Code Property Graph (CPG) for the vulnerable driver:

/.shiftleft/ocular/fuzzyc2cpg.sh <path/to/kernel/linux/drivers/scsi/aacraid>

Once you have the CPG, start Ocular:

sl ocular

When prompted, load the CPG you generated:

importCpg("<path-to-cpg.bin.zip>")

Run workspace to ensure that your CPG has been loaded. If loaded is true, you're ready to proceed with your investigative queries.

Examine the Code

Examine the interaction from user to kernel space with copy_from_user. To determine if any data from user space to kernel space is copied, use:

cpg.call.name("copy_from_user").code.p

Ocular returns the following:

copy_from_user(&qd, arg, sizeof (struct aac_query_disk))
copy_from_user(&dd, arg, sizeof (struct aac_delete_disk))
copy_from_user(&dd, arg, sizeof (struct aac_delete_disk))
copy_from_user((void *)kfib, arg, sizeof(struct aac_fibhdr))
copy_from_user(kfib, arg, size)
copy_from_user((void *)&f, arg, sizeof(struct fib_ioctl))
copy_from_user(&fibsize, &user_srb->count,sizeof(u32))
copy_from_user(user_srbcmd, user_srb,fibsize)
copy_from_user(p,sg_user[i],upsg->sg[i].count)
copy_from_user(p,sg_user[i],upsg->sg[i].count)
copy_from_user(p,sg_user[i],usg->sg[i].count)
copy_from_user(p, sg_user[i],\n\t\t\t\t\t\t\tupsg->sg[i].count)

This indicates that there doesn't seem to be any problems with data from user space to kernel space.

To look at flows from copy_from_user, use

def sinkArguments = cpg.method.name("copy_from_user").parameter.argument
println(sinkArguments.reachableBy(cpg.identifier).flows.p)

The query println(sinkArguments.reachableByFlows(cpg.identifier).l.size) returns the number 302.

Of interest is an estimate determining if the arguments of copy_from_user are sanitized. There are no direct definitions at if expressions, but information that flows into if expressions is available. To access this information using Main.scala, add the following lines

val reachingDefs1 = cpg.method
.name("copy_from_user")
.parameter
.argument
.reachableBy(cpg.identifier)
.toSet

reachableByFlows is used to construct and print out the flows. To filter all that detail, use reachableBy to have Ocular identify only the sources that are hit, rather than the details of the data flow paths. The following query collects the sources that are hit as a set

ocular> val reachingDefs2 = cpg.method
.name(".*less.*", ".*greater.*")
.parameter
.argument
.reachableBy(cpg.identifier)
.toSet

This query:

  • Restricts the flows running to expressions that involve the less or greater keyword. Note that internally each binary operation (+,-,>,< etc.) is also treated as a function
  • Tracks data dependency back to each identifier it hits and collected into a set

Now, check to see if there is an intersection between these two sets, which provides an estimate on which arguments of copy_from_user might be sanitized

reachingDefs1.intersect(reachingDefs2).foreach(elem => println(elem.code))

This query returns the following:

kmalloc(fibsize, GFP_KERNEL)
kmalloc(actual_fibsize - sizeof(struct aac_srb)\n\t\t\t + sizeof(struct sgmap), GFP_KERNEL)
user_srbcmd->sg
kfib->header
size = le16_to_cpu(kfib->header.Size) + sizeof(struct aac_fibhdr)
actual_fibsize = sizeof(struct aac_srb) - sizeof(struct sgentry) +\n\t\t((user_srbcmd->sg.count & 0xff) * sizeof(struct sgentry))
* upsg = (struct user_sgmap64*)&user_srbcmd->sg
i = 0
user_srbcmd->sg
usg = kmalloc(actual_fibsize - sizeof(struct aac_srb)\n\t\t\t + sizeof(struct sgmap), GFP_KERNEL)
user_srbcmd->sg
* upsg = &user_srbcmd->sg
user_srbcmd = kmalloc(fibsize, GFP_KERNEL)
i = 0
fibsize = 0
aac_fib_alloc(dev)
user_srbcmd->sg.count
kfib = fibptr->hw_fib_va
kfib->header.Size
actual_fibsize - sizeof(struct aac_srb)\n\t\t\t + sizeof(struct sgmap)
fibptr = aac_fib_alloc(dev)
i = 0
fibptr->hw_fib_va
i = 0

The return shows that most potential checks involve some kind of a size element as expected.

Some outputs from copy_from_user have kfib as their first argument, which may be a pointer giving access to a header. The size of kfib seems to be involved with check kfib->header.Size. To confirm this in the source code (commctrl.c, line 90), use:

size = le16_to_cpu(kfib->header.Size) + sizeof(struct aac_fibhdr);
if (size < le16_to_cpu(kfib->header.SenderSize))

Use one of the following two queries to filter for copy_from_user looking for kfib as an argument:

cpg.call.name("copy_from_user").code(".*kfib.*").l

or

cpg.call.name("copy_from_user").filter(call => call.argument.code(".*kfib.*")).l

To print the return:

cpg.call.name("copy_from_user").filter(call => call.argument.code(".*kfib.*")).l.foreach(call => println(call.code))

You should see output as follows:

copy_from_user((void *)kfib, arg, sizeof(struct aac_fibhdr))
copy_from_user(kfib, arg, size)

Next, find the data flow from these sinks to a common ancestor which defines kfib. This helps ensure that there is no other definition of kfib which might have a double fetch.

val cfu1 = cpg.call.name("copy_from_user").code(".*kfib.*").l.head.start.reachableBy(cpg.identifier).toSet
val cfu2 = cpg.call.name("copy_from_user").code(".*kfib.*").l.last.start.reachableBy(cpg.identifier).toSet.intersect(cfu1)
cfu2.foreach(elem => println(elem.code + " " + elem.lineNumber.get)

This pattern is similar to reaching definitions to sanitizers. The start tells Ocular to start a fresh traversal at the given node. In this case, head and last are filtered to get those nodes. The output is as follows:

(aac_fib_alloc(dev),71)
(kfib = fibptr->hw_fib_va,76)
(fibptr = aac_fib_alloc(dev),71)
(fibptr->hw_fib_va,76)