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
>
*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