Reputation: 31
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
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.
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
#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: -
Upvotes: 1