C++ Logo

sg14

Advanced search

Re: Memory Safety and Page Protected Memory

From: James Mitchell <james_at_[hidden]>
Date: Sat, 2 Mar 2024 14:52:51 +1100
Adding my thoughts on the other things being discussed here:

*Guard Pages*

While guard pages from tools like electric fence are useful, they don't
tend to scale with larger applications as you run out of memory very
quickly as every allocation regardless of how small ends up using at a
minimum the entire page. They are also more of a tool to detect buffer
overflows not necessarily prevent them; it will prevent a buffer overflow
which is bounded by a certain size but an unbounded read primitive can read
any part of memory.

They are also limited to heap allocations, you could attempt to do
something similar with the stack but would require code gen changes, so you
could put every stack frame into its own region surrounded by uncommitted
pages, if the committing and releasing of those was done at runtime it
would be huge overhead, you could pre-populate alternating pages with
reserve/commit and have a unique one per stack frame but then you would
need to special case any stack frame which is more than the page size.

*Valgrind*

Valgrind has a lower memory footprint then using guard pages, but because
it's essentially emulation it has a significant runtime overhead which for
some applications makes it virtually unusable for running as part of the
normal workflow except over things like unit-tests.

*Address Sanitizer*

I haven't seen this mention, but it's a big leap forward in the space which
Guard pages and Valgrind which are essentially external from the
compilation process, instead it changes how the code compiles so every load
and store validates shadow memory for if it is poisoned or not, with custom
memory allocators which poison/unpoison memory.

I find it to be much better and easier to integrate into existing memory
allocators, while having better performance then both Valgrind and Guard
Pages. It's still not bulletproof as if you read memory out of bounds that
isn't poisoned then it will not detect it.

*Secrets Protection*

There is some discussion around using page protection in order to hide
secrets, while I can see the use case I would architect things differently
to where these secrets don't need to exist either long term within the
program. A few examples of how this can be achieved are:

   - Secrets for things like databases which are handled during
   initialization should not exist after it's connected, this means don't use
   environment variables, don't use command line arguments or anything which
   persists from an external source. Storing in a file would be a better
   approach or if you can OS supplied secrets key/value pairs area, get the
   value use it then memset_explicit it out of existence, as both require
   remote code execution not just a read primitive to get it after it's gone.
   There are more advanced approaches you could use like supplying the program
   with one time usable tokens, but that is more complex to implement and use.
   - Secrets/passwords from users, these should be short lived also and if
   possible never actually exist in the program this could be done using a few
   approaches one is to store the hashed (and salted) version of the password
   in memory not the actual password, this way if it does leak then the
   password isn't able to be used without first cracking the password. Another
   approach is to instead separate authentication from your main application
   so passwords never end up touching it but instead you have a specific
   application which will take the authentication details and give you a token
   which can be used for some period of time.

Aside from memset_explicit the only thing which I could see might fit
within standardization would be methods to securely hash passwords,
generate and validate tokens, etc but it feels like that's best suited for
third party libraries.

*Read/write primitives, remote code execution*

I understand that most people probably aren't aware of how code is
exploited and what's involved, especially when it comes to memory safety,
there are two main parts to crafting an exploit:


   - A read primitive - This lets you read from memory, it can help
      - Discover memory addresses for overcoming techniques like address
      space layout randomization (ASLR) which makes your code and
stack be stored
      in random location every time you program starts
      - Discover stack canaries which are used to prevent contiguous buffer
      overflows of stack memory overwriting the return address
      - Discover secrets such as passwords, keys, personal information, etc
      - Reads could be relative to another address (e.g. an array) or
      absolute (just a pointer), for any read that is relative it is easier to
      discover ASLR addresses and stack canaries if it's relative to the stack,
      if the address is absolute then with ASLR it becomes significantly harder
      to exploit as you don't know what address to access, using the wrong one
      will segfault and a new running program will be a new address space
      (forking can actually lead to this being bypassable as the address space
      and stack canaries don't change)
   - A write primitive - This lets you write to memory, it can help you
   - Overwrite the return address on the stack with one which executes
      blocks of code when the function returns, in modern programs
compiled with
      ASLR and Canaries you need a read primitive
      - Overwrite the address of a function pointer to point to code that
      you want
      - Overwrite any permissions or other validation data used to validate
      - Overwrite anything else to be malicious and cause damage
   - Remote code execution
      - In a program compiled with stack canaries and ASLR this typically
      requires a read and write primitive to achieve
      - Shell code injection is a thing of the past, modern systems have
      W^X (Write or execute, never both) page restrictions
      - Modern remote code execution relies on return oriented programming
      (ROP) where you overwrite the return address on the stack to point to
      another region of executable memory, typically the end of
another function
      which might do something that you want (e.g. store X in a register, call
      the address in register X, issue syscall) these blocks of a few
      instructions at the end of functions are called ROP-gadgets and attackers
      build a sequence of these to do more complex things. There are
tools which
      make crafting these ROP-chains easier so it's not all by hand and as
      difficult as it sounds.


*Safe C++ code*

There are a few things which I commonly see people get wrong when
attempting to write safe C++ code especially from people who typically
write high-performance code

   - Not knowing C's sharp edges, the average C++ programmer can probably
   tell you strcpy is unsafe, however their solution would probably be to use
   strncpy not knowing that it is also in-fact not safe because it won't
   terminate. The failure of Annex K showed that we are not in a good place
   for having consistent safe functions to use, it would be nice to see these
   addressed.
   - The reason I mentioned C was because many people believe (imho without
   much foundation) that the closest C++ is to C that the faster it will be,
   unfortunately our standard library doesn't give much help nearly every
   single container allocates memory by default, if you want something which
   doesn't allocate memory so many people fall back to C (e.g. char[N], T[N],
   etc)
   - For C++ containers nearly everyone uses the unchecked accessors (e.g.
   vector operator[]) instead of the "safer" throwing versions (vector::at),
   the most likely reason for this I suspect is that people are concerned
   about exceptions (like myself) or they are concerned about the runtime
   overhead, it would be good to see checked versions which didn't throw.
   - Lifetimes and ownership, it's pretty common to store raw pointers and
   references to things which you expect will have their lifetime bound to
   something else, so _should_ be safe. Circumstances change, edge cases
   happen and suddenly the lifetime isn't what was expected. It's easy to say
   use smart pointers but they add their extra overhead isn't great when you
   ideally shouldn't need it.

I think it's worth digging into what could be done with C++ with
regards to non-lexical
lifetimes <https://rust-lang.github.io/rfcs/2094-nll.html> similar to Rust,
Sean Baxter has done a bunch of interesting work with borrow checking in
Circle.

Thanks,
James Mitchell




On Thu, 29 Feb 2024 at 19:53, Tiago Freire via SG14 <sg14_at_[hidden]>
wrote:

>
> > Let's say you designate a piece of memory intended to store passwords.
> How would you prevent the application from accessing it when in parts "when
> the access is illegitimate"
>
> Like this...
>
> void password_check()
> { char username[20];
> char password[20];
> char* page_protected_password = (char*) memory_safe_alloc(20);
> get_user(username);
> get_pass(password);
> do_something_stupid(username);
> }
>
> void do_something_stupid(char* username)
> { // not this obvious, but code in this function does:
> char* password = username+20;// uh, oh!
> ...
> }
>
> The page_protected_password is in its own bubble. A programmer can't land
> in the page_protected_password buffer by overrunning some nearby pointer
> like username. Nor can the programmer overrun the page_protected_password
> buffer without a segfault or signal triggered.
>
> You are right if thinking that get_pass(page_protected_password), or any
> function passed that pointer, may do whatever it likes with
> page_protected_password. Being memory safe gives protection from buffer
> overruns, doesn't mean error-proof.
>
> Thoughts?
>
>
>
> Ok, I can totally see the possible construction of a class let's call it
> "ProtectedMemory", that could be used like this:
>
> ProtectedMemory sensitive_data;
> sensitive_data.allocate(size, alignment);
> sensitive_data.unlock(); //switches to OS
> privileged mode to unlock the data and returns to caller
> legitimate_use_function(sensitive_data.data()); //ok
> sensitive_data.lock(); //switches to OS
> privileged mode to lock the data and returns to caller
> sneaky_bad_function(sensitive_data.data()); //segfaults
>
>
> Now this wouldn't be full proof, you would still be able to have
> illegitimate access to sensitive memory while the buffer is unlocked (not
> entirely sure if it is possible to limit access to specific threads only),
> and it wouldn't do much if a malicious attacker gets random code execution.
> But it may be able to make it harder to access via a buffer overrun even if
> it doesn't stop it completely.
>
> This of course will require some cooperation of the operating system to be
> able to implement it, and lets hope it doesn't cause a more serious bug
> where a badly crafted unlock command unlocks access to memory in a
> different application. But it looks feasible in theory, but someone with a
> better understanding of the topic might be able to weigh in on this.
> _______________________________________________
> SG14 mailing list
> SG14_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/sg14
>

Received on 2024-03-02 03:53:05