Lecture 23: Analyzing information flow

Topics:

Review of noninterference

We want to enforce information security properties such as confidentiality and integrity. The problem is that we can't trust programs. Even if they aren't malicious Trojan horses, programmer mistakes can violate these properties. And conventional, discretionary access control doesn't control information propagation.

We've seen that these information security requirements can be expressed to some degree using the idea of noninterference, which is a mathematical formalization of the notion that there is zero information flow from one set of information (the high information) to another set (the low information). Intuitively, noninterference says that you should not be able to learn anything about the high inputs to a system by observing the low inputs and outputs.

Let us use s to denote a state of the system and [s] to be the behavior of the system starting from state s (we used double brackets in class. If your browser fully supports them, these will have the right appearance: 〚s〛 and ⟦s⟧). Then we can describe the observational capability of the low observer as indistinguishability relations on states or behaviors. Typically these are equivalence relations, but not always. Suppose we write s1L s2 to mean that the low observer cannot distinguish between s1 and s2, and similarly we write [s1] ≈L [s2] to mean that outputs/behaviors [s1] and [s2] are indistinguishable. Then we can express noninterference as an implication:

s1L s2 ⇒ [s1] ≈L [s2]

This is a very abstract description, and its precise significance depends on how we define the states, the behaviors, and the indistinguishability relations. We might consider states and behaviors to be more or less the same thing, where a behavior [s] for a given state s is simply the final state that results after running the system starting from state s. Another natural choice is to consider the behaviors to be the entire traces of states that are encountered as the system runs.

Covert vs. overt channels

Channels are conduits for information. Since we want to control how information flows (and hopefully enforce noninterference), it is useful to talk about the different ways this can happen. Butler Lampson classifies channels into covert and overt channels. Overt channels are those that are clearly intended to transmit information. Covert channels are those whose ostensible purpose is not the transmission of information.

Lampson also draws a distinction between storage channels and timing channels. The former transmit information through explicit changes of system state; the latter, by changing the amount of time that events take. Timing channels are almost always covert channels; programs that intentionally transmit information through timing of events must exploit unreliable properties of the system they are running on.

In systems with concurrent threads, it becomes possible to convert storage channels into timing channels and vice versa, using synchronization between different threads.

Static analysis of information flow

Suppose we have a program we want to analyze, containing both high and low variables. We will consider two states of the running program to be indistinguishable if and only if they agree on all their low variables. We will write a subscript H or L to indicate what sort of variable we are dealing with, e.g. xL or yH. We will write an underscore under a variable when we are talking about the label of that variable, e.g. x is the label of variable x.

Now consider some assignment statements that update the state:

  1. xH = yH // ok
  2. xH = yL // ok
  3. xL = yH // bad
  4. xL = yL // maybe bad

The only surprise here is perhaps #4. Why could assigning from a low variable to a low variable be a problem? The issue is that information can flow from control flow in the program. In the wrong context, an assignment from low can cause this information flow to be insecure. For example, consider this code:

xL = 0;
if (yH) {
  xL = 1;
}

This program contains what is known as an implicit flow from variable yH to xL. Assuming that yH is either zero or one, the program is equivalent to the assignment xL = yH. An implicit flow is an example of a covert storage channel.

One way to statically analyze implicit flows is to introduce a label for a pseudo-variable pc, representing the program counter. The idea is that at a given point in the program, the label pc represents the information learned from the fact that the program counter got to that point. Within an if test with a guard of yH, as above, the program-counter label is set to the label of the guard, i.e, H.

To check information flows created by an assignment x=y, we see that there is an explicit from y to x, and so we must check yx. Note that we are using the simple partial order here in which L ⊑ H but not the reverse. The same check works even if we are using a more complex partial order.

Here is a more curious case:

while (x) {
    // do some statements c
}

The usual approach to checking this is to treat it like an if statement, checking commands c with the pc adjusted according to x. Consider this program, though:

while (yH != 0) {}

A attacker who can observe whether the program terminates learns the value of a high variable. An analysis that consider this secure is only enforcing termination-insensitive noninterference. Mathematically, termination is considered indistinguishable from all other outcomes. (This means that indistinguishability is not a transitive relation; all that is required of it is that it is reflexive and symmetric.) However, termination-sensitive noninterference is hard to enforce accurately because you have to prove that programs terminate.

This termination channel only leaks a bit of information. It's an instance of a timing channel in which the time taken by code conveys information. For example:

yH = yH * 1000000;
while (yH > 0) { yH--; }

An attacker who can time this learns information. If time is not part of the system state, then noninterference as we've defined is also timing-insensitive.

Jif

Jif is the most complete design and implementation of a language that supports static information flow. It has object, methods, and exceptions. Label inference and default labels are used to reduce the annotation burden. Code can be parameterized on labels (and therefore generic with respect to them). Here is a variable declaration in Jif:
int {L} x
The expression L is a label. The type of x is a security type int{L}. Labels can be left out:
float cos (float x) {
    ...
}

Here the variable x can be called with a value of any label—the label of x is an implicit parameter to the code, which doesn't get passed at run time. The result of the function isn't labeled either. The default result label is the join of the labels of all the parameters. In this case the result label is the label of x, which is probably what we want for cosine.

In Jif, classes can also have label parameters. Here's how we might declare a storage cell that can hold a value with any label:

class Cell[label L] {
    int {L} value;
    int {L} get() {
	return value;
    }
    void set{L}(int newv) {
	value = newv;
    }
}
This class can be instantiated with any label L. The method set has a begin label that prevents it from being called from any program counter less than L. This is important in order to prevent implicit flows into the field value.