[apparmor] stack and stack_onexec semantics again

John Johansen john.johansen at canonical.com
Wed Mar 9 01:00:36 UTC 2016


We need to settle on the exact semantics stack and stack_onexec. This
topic has been discussed before but without proper resolution.

First lets look at the semantics of change_profile and change_onexec

change_profile allows changing the profile immediately, while
change_onexec allows delaying the change until the exec barrier. The
change_profile api is really a special use api because of the
limitations around memory and fd revalidation. It can be used to provide
a different profile for an executable, but in those cases B becomes a
special stub profile.

That is given a process 1 confined by profile A, an executable /e with
the following defined policy

  profile A {
     change_profile -> B,
     px /e -> D,
  }

  # stub profile
  profile B {
     include "stub_rules",

     px /e -> C,
  }

  profile C {
    ...
  }

  profile D {
    ...
  }

  profile E {
     ix /e,
  }

if process 1 does NOT call change_profile when it execs /e it will change
into profile D
if process 1 does a change_profile (the only change allowed) it will end
up in B and then /e is execed it will change into profile C

Of note is that change_profile allows changing the immediate confinement
(obvious) but also the confinement of the exec. Also of note is that the
stub profile contains a set of rules necessary to perform the necessary
operations between the change_profile and the exec.


We can get rid of the stub profile by using change_onexec.  This requires
changing profile A into A' as follows

  profile A' {
     include "stub_rules",

     change_profile /e -> C,
  }

Of note is that A' now includes the stub profile's set of rules, and the
change_profile rule is changed to have and exec constraint. The use of
change_onexec simplifies policy but change_profile does have its use in
a priv sep situation where it is desirable to separate the stub rules
from the profile.



Now on to stack, and stack_onexec, which allow for a different way to
change the tasks confinement. Instead of transitioning to a new profile,
a stack of multiple profiles is created, with the effective permissions
being the intersection of the set. The question is how should be stacking
rules be restricted and what are the exact semantics of the stack_onexec
case. The first thing to note is that a stack strictly reduces the
permission set, and that each profile in the stack determines its own
transitions.

So lets say process 1 confined by profile, calls aa_stack(E) resulting in
a confinement of A//&E, when the process then execs /e the following
happens

  A -> D
  E -> E

resulting in a new stack of D//&E

Since E can not grant privilege not allowed by A or D, at first glance
aa_stack() does not strictly need to be controlled by a rule, which would
be good for the potential use where a user wants to further restrict
system policy without modifying it directly and dealing with the package
management grief that entails. However there are some complications, it
is possible that a stack could result in a process being denied something
it normally wouldn't which could include dropping of capabilities or
transitioning to a new executable with a different confinement. Ideally
these situation would be properly handled by the application but lets go
with the assumption that appliations are buggy. So can an attacker
leverage the stack api and a buggy application to gain privileges?
Unfortunately in some cases yes.

Imagine the app has some form of privsep mode, obtained by dropping
privileges or transitioning a forked child to an executable. If the
privsep setup succeeds the app is happy but it is designed to work in
environments where setting up the privsep fails (or it just doesn't
handle the error) and continues to work without the privsep. The
question is does this actually matter for the stacking api? In all but
maybe a few cases the answer is actually no, because for an attacker to
be in a situation that they can influence the confinement by calling the
stacking api, or at least influencing what it is called with, they
already have access to the larger permission set, and likely other attack
options available to them. It is possible that the code that is being
exploited is after the insertion of a call to the stack api, but again
either the attacker has to influence what the application is specifying
in an existing api call or it is managing to insert a new stack api call.


The stack_onexec api however is a different story dependent on when/how
the stacking is applied. There are 3 places/ways it can be applied.

1. straight override applied to current confinement when stack_onexec is
   called
  ie.  A + stack_onexc(B) results in A//&B

2. applied to current profile at the start of the exec, but the exec
   transition is applied
  ie. using the policy from above and execing /e
   A + stack_onexec(B)  -> A//&B which then transitions to -> D//&C

3. applied to transition profile after the exec transition is applied.
  A -> D + stack(B) resulting in  D//&B


#1 allows overriding exec transitions and allows a potential attack on
   policy without strict rules to control it

#2 mimics the behavior of calling aa_stack() except that the stacked
   profile won't be used for anything except its transition

#3 this allows allow for a transition that does not override the domain
   transition and could be used with few to no rules to control it (ie.
   it is not vulnerable to the attack in #1)



Before going any further lets look at a parallel to stacking, stacked (or
compound) transition via change_profile.

It is possible for change_profile to specify a stacked target. That is

  aa_change_profile(B//&E)
or
  aa_change_onexec(C//&E)

where the necessary change_profile rules would be
  profile A {
    change_profile -> B//&E,
    ...
  }

  profile A'' {
    change_profile /e -> C//&E,
  }

currently we would also accept
  profile A {
     change_profile -> B,
     change_profile -> E,
     ...
  }

and
  profile A'' {
     change_profile /e -> C,
     chagne_profile /e -> E,
     ...
  }

as the final result is a subset of both of these. However these allow a
broader set than is likely desired as it allows transitioning to C or E
without the stack. Whether we want to keep this later form is a bit of
an open question but it does allow for subset construction without
having to specify each possible combination.

This change_profile example is interesting in that it shows standard
change_profile rules can be applied to control the stack. Whether we want
a different rule or extension to indicate that only stacking is allowed
is still an open question.
  ie.
     change_profile -> &B,
     change_profile -> &B//&E,
  or
     stack_profile -> B,


now one more point to add before we get back to considering the semantics
of the stack profile api. In the current iteration change_profile and
stacking only apply to the current namespace. That is to say if a task is
confined by multiple profiles across different namespaces, at this time
self directed transitions (stack, change_profile) only apply to the
profiles in the current namespace (lowest namespace in the stack).

  Eg. If a task is confined by
    parent_profile//&:child_ns://child_profile

  change_profile(A) would result in
    parent_profile//&:child_ns://A

  and aa_stack(A) would result in
    parent_profile//&:child_ns://child_profile//&:child_ns://A

  with the ns spec being repeated to be very explicit about which ns the
  profile being stacked belongs to

The important point to note here, is that we don't need to consider
stacking/and change_profile across namespaces except for where they add a
new namespace to the confinement. ie. We could do
  change_profile(:child_ns://A)

to transition into a child namespace, or
  aa_stack(:child_ns://child_profile)

to transition from
  parent_profile

to the stack of
  parent_profile//&:child_ns://child_profile

However these cases are covered by standard change_profile rules.

Finally we can get back to stacking

So how is stacking different than using change_profile with stack?
Technically it isn't. It is the same as doing
  confinement = aa_getcon()
  confinement += //&stack
  aa_change_profile(confinement)

what it does allow for is a task not to need to know about its current
confinement. It also provides the opportunity to be slightly lazier and
looser with policy where a stacking to reduce permissions could be
allowed without policy require change_profile rules, and it also provies
the opportunity to play with the semantics of stack_onexec.

So in the end the questions are?

1. Do we want to provide access to stacking without rules, or reduced
rule requirements.
- this can be allowed in the straight stacking without much risk
- this is problematic for stack_onexec, but see 2 below


2. Which semantics of stack_onexec should we go with?
It seems clear to me that #1 is the way to go as it is equivalent to
doing aa_getcon(), concat, aa_change_profile() as outlined above.
However this requires change_profile rules other wise it can be used to
over ride policy transitions when it shouldn't. If we want to allow for
stacking without requiring change_profile rules we could the requirements
some, by
  1. allowing the stack_onexec if the exec transition is respected
     eg. if a task is confined by profile E and it stacks C
        E -> E//&C
     since the exec transition E -> E is specified in the profile (ix)
     then stacking C on top is no different than doing a regular stack,
     as it doesn't override the exec transition. This allows for
     stacking without requiring the stub profile until the exec
     transition. That is to say that it has a parallel to what is done
     with change_profile vs change_onexec.
     ie. in task 1 is confined by E then
       E + stack(B) -> E//&B -> exec /e -> E//&C

     where B is the stub profile and
       E + stack_onexec(C) -> exec /e -> E//&C


  2. allowing the stack_onexec if there is a change_profile rule for the
  existing labeling.
    This is similar to point 1 above but instead of relying on just the
    exec we could also allow transitions allowed by change_profile for
    the current confinement.
      eg. if a task is confined by profile A' which allows
        change_profile /e -> C,

      then a change_onexec() or stack_onexec() that resulted in
        C//&something would be allowed as
        A'' is allowed to be replaced by C in the basic change_onexec() case.

I can't see another way to loosen the requirements for requiring
change_profile rules except the #3 proposal above which does NOT
correspond with the constructed behavior of stacking ie. aa_getcon(),
concat, and aa_change_profile() so I find it surprising and less than
intuitive.



More information about the AppArmor mailing list