Idea about implementing a SECCOMP alike mechanism in BEAM

classic Classic list List threaded Threaded
2 messages Options
Reply | Threaded
Open this post in threaded view
|

Idea about implementing a SECCOMP alike mechanism in BEAM

Henrique Marcomini
Hi,

  I've been working with erlang/elixir for the past year and I really miss
SECCOMP like features in the BEAM. So I started an implementation based on
https://github.com/erlang/otp and I wanted to know what you people think about
it.

  The idea is to provide a way to blacklist/whitelist function calls on a
process level, so for example if I'm evaluating human input for a calculation,
I can guarantee that no other function except the ones that are necessary are
called. Going further, in a case where my erlang cookie is leaked, I know
that only a limited set of functions are callable using rpc or node.spawn/2.

  The way I envision it (and I'm implementing it) is adding a byte to the
process struct with the following meaning:

   0 1 2 3 4 5 6 7
  +-+-+-+-+-+-+-+-+
  |E|M|S|I|U|U|U|U|
  +-+-+-+-+-+-+-+-+

  Where:
    E -> Whether the mechanism is active (0:Off/1:On)
    M -> Operation Mode (0:Whitelist/1:Blacklist)
    S -> Disable Spawn (0:Can spawn new process/1:Cannot spawn new process)
    I -> Whether a child process will inherit the
    U -> Unused

  There are some implicit rule in this byte:
    - M,S, and I are unused whether E is set to 0
    - I is unused if S is set to 1

  I choosed to use a byte because bitwise operation are cheap and are the least
expensive way I could think, and bitmasks can be combined in a meaningful way.

  The verification of this byte would occur at the apply function, so we can
check the byte every time a function is called. To know which function is
whitelisted/blacklisted I added an Eterm to the process struct. This Eterm is a
NIL terminated list of tuples, each tuple contains two atoms representing the
module name and the function name which is whitelisted/blacklisted.

  Probably a hashmap, or a binary tree of hashs would be quicker to search.
But I don't know if there is any good low level way to introduce it without
adding a lot of code to the code base.

  To implement process inherit capabilites, I added a verification on spawn,
but there are some possible bypasses that would need to be treated latter on.

  For example:

-------------------------------------------------------------------------------

  If there is a process running as a dynamic supervisor (P1), some other
process (P2) may send a message to spawn some worker (P5) and the father
process would be the supervisor (P1), which may not have the mechanism active.

Diagram below:


             |                              |
             |  P1 - Dynamic Supervisor     | P2 (With active mechanism)
             V                              |
      ===============                       |
      | P3   | P4                           V
      V      V

  When P2 asks P1 to spawn a new worker, the diagram will look like the
following:


             |                              |
             |  P1 - Dynamic Supervisor     | P2 (With active mechanism)
             V                              |
      ===============                       |
      | P3   | P4   | P5                    V
      V      V      V

  Where P3, P4, and P5 are spawned with P1 as a parent, so it will not inherit
any rules from P2. At this point P5 can execute any code and send a message to
P2, bypassing the mechanism.

-------------------------------------------------------------------------------

  On another case a process (P1) on Node 1 which is under this mechanism may
spawn another process (P2) to Node 2, and then P1 spawns another process (P3)
on Node 1. If process generated by spawns of other nodes are less secure than
the process that called the spawn function, it will lead to privilege
escalation.

Diagram below:

 +---------------+              +---------------+
 |               |              |               |
 |    Node 1     |              |    Node 2     |
 |               |              |               |
 |        P1     |   spawn/2    |               |
 |   ------------> -------------> --------+     |
 |       calls   |              |         |     |
 |               |              |         | P2  |
 |               |              |         |     |
 |        P3     |   spawn/2    |  calls  |     |
 |   <---------- <------------- <---------+     |
 |               |              |               |
 +---------------+              +---------------+

  If P3 restrictions are less strict than P1, then P1 escalated privilege.

-------------------------------------------------------------------------------

  The code that I'm working is at https://github.com/Supitto/OTP/tree/maint
and it is built upon the maint branch. It still quite imature and have some
edges to trim (I'm still figthing with allocations and Eterms). But if this
idea is apreciated I will implement everything on the main branch. Also if
you can think of some other scenario where this mechanism is defeated, please
infome me :D

Thanks,

Henrique Almeida Marcomini

Telegram -> @supitto
IRC (freenode) -> Supitto

Ps. The code on the repo may be not working (depends on the commit), but the
    idea is there.

Pps. I made everything in ASCII so to see it properly use monospace fonts
Reply | Threaded
Open this post in threaded view
|

Re: Idea about implementing a SECCOMP alike mechanism in BEAM

Lukas Larsson-8
Hello,

It has been a while since it was published but maybe you will find this interesting: http://www.erlang.se/publications/xjobb/0109-naeser.pdf

Lukas

On Wed, Dec 11, 2019 at 6:07 AM Henrique Marcomini <[hidden email]> wrote:
Hi,

  I've been working with erlang/elixir for the past year and I really miss
SECCOMP like features in the BEAM. So I started an implementation based on
https://github.com/erlang/otp and I wanted to know what you people think about
it.

  The idea is to provide a way to blacklist/whitelist function calls on a
process level, so for example if I'm evaluating human input for a calculation,
I can guarantee that no other function except the ones that are necessary are
called. Going further, in a case where my erlang cookie is leaked, I know
that only a limited set of functions are callable using rpc or node.spawn/2.

  The way I envision it (and I'm implementing it) is adding a byte to the
process struct with the following meaning:

   0 1 2 3 4 5 6 7
  +-+-+-+-+-+-+-+-+
  |E|M|S|I|U|U|U|U|
  +-+-+-+-+-+-+-+-+

  Where:
    E -> Whether the mechanism is active (0:Off/1:On)
    M -> Operation Mode (0:Whitelist/1:Blacklist)
    S -> Disable Spawn (0:Can spawn new process/1:Cannot spawn new process)
    I -> Whether a child process will inherit the
    U -> Unused

  There are some implicit rule in this byte:
    - M,S, and I are unused whether E is set to 0
    - I is unused if S is set to 1

  I choosed to use a byte because bitwise operation are cheap and are the least
expensive way I could think, and bitmasks can be combined in a meaningful way.

  The verification of this byte would occur at the apply function, so we can
check the byte every time a function is called. To know which function is
whitelisted/blacklisted I added an Eterm to the process struct. This Eterm is a
NIL terminated list of tuples, each tuple contains two atoms representing the
module name and the function name which is whitelisted/blacklisted.

  Probably a hashmap, or a binary tree of hashs would be quicker to search.
But I don't know if there is any good low level way to introduce it without
adding a lot of code to the code base.

  To implement process inherit capabilites, I added a verification on spawn,
but there are some possible bypasses that would need to be treated latter on.

  For example:

-------------------------------------------------------------------------------

  If there is a process running as a dynamic supervisor (P1), some other
process (P2) may send a message to spawn some worker (P5) and the father
process would be the supervisor (P1), which may not have the mechanism active.

Diagram below:


             |                              |
             |  P1 - Dynamic Supervisor     | P2 (With active mechanism)
             V                              |
      ===============                       |
      | P3   | P4                           V
      V      V

  When P2 asks P1 to spawn a new worker, the diagram will look like the
following:


             |                              |
             |  P1 - Dynamic Supervisor     | P2 (With active mechanism)
             V                              |
      ===============                       |
      | P3   | P4   | P5                    V
      V      V      V

  Where P3, P4, and P5 are spawned with P1 as a parent, so it will not inherit
any rules from P2. At this point P5 can execute any code and send a message to
P2, bypassing the mechanism.

-------------------------------------------------------------------------------

  On another case a process (P1) on Node 1 which is under this mechanism may
spawn another process (P2) to Node 2, and then P1 spawns another process (P3)
on Node 1. If process generated by spawns of other nodes are less secure than
the process that called the spawn function, it will lead to privilege
escalation.

Diagram below:

 +---------------+              +---------------+
 |               |              |               |
 |    Node 1     |              |    Node 2     |
 |               |              |               |
 |        P1     |   spawn/2    |               |
 |   ------------> -------------> --------+     |
 |       calls   |              |         |     |
 |               |              |         | P2  |
 |               |              |         |     |
 |        P3     |   spawn/2    |  calls  |     |
 |   <---------- <------------- <---------+     |
 |               |              |               |
 +---------------+              +---------------+

  If P3 restrictions are less strict than P1, then P1 escalated privilege.

-------------------------------------------------------------------------------

  The code that I'm working is at https://github.com/Supitto/OTP/tree/maint
and it is built upon the maint branch. It still quite imature and have some
edges to trim (I'm still figthing with allocations and Eterms). But if this
idea is apreciated I will implement everything on the main branch. Also if
you can think of some other scenario where this mechanism is defeated, please
infome me :D

Thanks,

Henrique Almeida Marcomini

Telegram -> @supitto
IRC (freenode) -> Supitto

Ps. The code on the repo may be not working (depends on the commit), but the
    idea is there.

Pps. I made everything in ASCII so to see it properly use monospace fonts