Aaron
Aaron

Reputation: 31

How to securely communicate between a service and user program using Boost Interprocess?

I have a service with LOCAL SYSTEM privileges, and I want it to be able to communicate with my client application, which is run under unprivileged users (I'm on Windows). I am using boost::interprocess for "shared memory", which really just creates memory-mapped files under the hood. Right now I simply create a memory-mapped file with GENERIC_READ and GENERIC_WRITE given to everyone, so that the client can communicate with the service. However, I'm wondering what the current security risks of that are, and how to make this secure?

For one, someone could overwrite the data in the memory mapped file, interrupting communication between the client and the service. But I'm wondering if something more drastic is possible, like someone redirecting the service to other memory addresses to execute malicious code by editing the memory-mapped file. I don't store pointers within the memory-mapped file itself, I only use the boost::interprocess offset_ptr and plain data.

Upvotes: 1

Views: 500

Answers (1)

sehe
sehe

Reputation: 393134

Yes people tampering with your shared memory can compromise most applications.

No, using offset_ptr does not innoculate against that.

  • As a trivial counter example, imagine a linked list or graph using offset_ptr. If a malicious actor made a cycle in that list, it could lead to DoS by infinite loop if your application doesn't protect against that.

  • offset_ptr only allows pointer values to be interpreted as the equivalent addresses in separate process spaces, but it provides no validation of the actual value. A simple counter-example here is when you make the pointer point outside the valid range for the shared memory region. It doesn't matter that you store it using offset_ptr, the pointer value will still be invalid.

Access Control

What you seem to be after is access control. This is hard to achieve portably, and as such only limited control is present in Boost Interprocess, in the form of boost::interprocess::permissions:

// In header: <boost/interprocess/permissions.hpp>


class permissions {
public:
  // construct/copy/destruct
  permissions(os_permissions_type) noexcept;
  permissions() noexcept;

  // public member functions
  void set_default() noexcept;
  void set_unrestricted() noexcept;
  void set_permissions(os_permissions_type) noexcept;
  os_permissions_type get_permissions() const noexcept;
};

The default permissions are a null SECURITY_ATTRIBUTES on windows or 644 filemode on POSIX (u=rw,go=r).

To restrict read and write access to the owner (effectively the user creating the shared memory object) on linux, use e.g. 0600.

Demo

Coliru

#include <boost/interprocess/mapped_region.hpp>
#include <boost/interprocess/permissions.hpp>
#include <boost/interprocess/shared_memory_object.hpp>
#include <boost/interprocess/sync/interprocess_mutex.hpp>
#include <mutex>

// demo data structure
#include <boost/container/static_vector.hpp>
#include <boost/range/iterator_range.hpp>
#include <fmt/ranges.h>

namespace bip = boost::interprocess;
namespace bc = boost::container;

struct SharedData {
    struct Layout {
        using Lock = std::unique_lock<bip::interprocess_mutex>;

        bip::interprocess_mutex mutable mutex_;
        bc::static_vector<int, 1024> vec_{};

        Lock obtainLock() const { return Lock(mutex_); }

        void add(int i) {
            auto lk = obtainLock();
            vec_.push_back(i);
        }
    };

    bip::shared_memory_object _smo = [] {
        bip::permissions          _perms{0600}; // u=rw
        bip::shared_memory_object smo(bip::open_or_create, "demo_shared",
                                      bip::mode_t::read_write, _perms);
        smo.truncate(sizeof(Layout));
        return smo;
    }();
    bip::mapped_region _mr{_smo, bip::mode_t::read_write, 0, sizeof(Layout)};

    void add(int i) { raw().add(i); }
    auto list() {
        auto lk = raw().obtainLock();
        return std::make_pair(std::move(lk),
                              boost::make_iterator_range(raw().vec_));
    }

  private:
    Layout& raw() { return *reinterpret_cast<Layout*>(_mr.get_address()); }
};

int main(int argc, char** argv) {
    auto const addable = std::vector(argv + 1, argv + argc);
    SharedData data;

    auto dump = [&data] {
        auto [lk, ls] = data.list();
        fmt::print("Existing: {}\n", ls);
    };

    dump();

    for (auto i : addable)
    {
        fmt::print("Adding element: {}\n", i);
        data.add(std::stoi(i));

        dump();
    }
}

When run with e.g. sotest 1 2 3 prints e.g.

$ ./build/sotest 1
Existing: []
Adding element: 1
Existing: [1]

$ ./build/sotest 2 3 4
Existing: [1]
Adding element: 2
Existing: [1, 2]
Adding element: 3
Existing: [1, 2, 3]
Adding element: 4
Existing: [1, 2, 3, 4]

And the shared memory mode is -rw-------:

stat /dev/shm/demo_shared 
  File: /dev/shm/demo_shared
  Size: 4144            Blocks: 16         IO Block: 4096   regular file
Device: 18h/24d Inode: 29          Links: 1
Access: (0600/-rw-------)  Uid: ( 2000/    sehe)   Gid: ( 2000/    sehe)
Access: 2022-06-28 00:25:47.167492907 +0200
Modify: 2022-06-28 00:25:10.559796466 +0200
Change: 2022-06-28 00:25:10.559796466 +0200
 Birth: -

enter image description here

Upvotes: 1

Related Questions