Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clear sensitive memory without getting optimized out #636

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

real-or-random
Copy link
Contributor

This is a rebase of #448, including a few changes (mostly implementing what I described in #185). I haven't tested this on Windows and I actually haven't reviewed this in detail so far, but people can start to have a look.

Fixes #185 and closes #448.

src/util.h Outdated
#if defined(_MSC_VER)
/* SecureZeroMemory is guaranteed not to be optimized out by MSVC. */
SecureZeroMemory(ptr, n);
#elif defined(__GNUC__)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't figure out when GCC started to support assembly but it's some version <= 2.95... So if you have such an old compiler, I guess you're on your own.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GCC 2.95 is likely the earliest we'd would ever encounter someone wanting to support. (Haiku OS uses it still, I believe :) )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think this is fine.

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>

#if defined(_MSC_VER)
// For SecureZeroMemory
#include <Windows.h>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's okay but I'm not entirely sure if we want that. It's used in Bitcoin Core too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be that this should be version gated, I'll see if I can figure out where thats supported to.

@gmaxwell
Copy link
Contributor

gmaxwell commented Jun 6, 2019

I think using a sizeof on an argument is likely to introduce almost impossible to detect bugs when e.g. someone hands it the first element of an array (esp one passed from another function). It doesn't feel particularly typesafe to me.

Is there a reason why you preferred this approach to having every subsystem (like field, scalar) define a clear function for its relevant type?

@real-or-random
Copy link
Contributor Author

real-or-random commented Jun 7, 2019

No, I don't prefer that to be honest. It's just the approach taken in #448, and I was somewhat worried about it, too.

I'll try to change it.

@real-or-random
Copy link
Contributor Author

I removed the macro from the PR. Also I removed _fe_set_zero from the PR (one can use _fe_set_int).

I'm happy to take suggestions for additional code locations where memory should be cleared.

src/util.h Outdated
* just not remove it entirely. See "Dead Store Elimination (Still) Considered Harmful" by
* Yang et al. (USENIX Security 2017) for more background.
*/
memset(ptr, 0, len);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that "clearing" is cleanly separated from "setting to zero", it's a good idea to replace 0 with a different value here for testing that the code does not rely on the fact that the "_clear" functions set to 0. We could actually always use a different value here, or use a different value in (some runs of) tests. What do people think about this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try checking the binary that it's actually not optimized out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet. But yes, we should do this with a few compilers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a good idea to replace 0 with a different value here for testing

I don't think it's a good idea to use a different value in testing. But I don't see a downside to just replace 0 with 42. This also has the (slight) advantage that it'll make it easier to detect now improper usage of *_clear when rebasing -zkp on top of this. -zkp uses *_clear for initializing in many places.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of using a non-zero constant byte value. Using 0 is more likely to result in silent failures when uninitialized memory is accidentally used.

@@ -49,10 +49,11 @@ static int secp256k1_fe_normalizes_to_zero(secp256k1_fe *r);
* implementation may optionally normalize the input, but this should not be relied upon. */
static int secp256k1_fe_normalizes_to_zero_var(secp256k1_fe *r);

/** Set a field element equal to a small integer. Resulting field element is normalized. */
/** Set a field element equal to a small integer. Resulting field element is normalized; it has
* magnitude 0 if a == 0, and magnitude 1 otherwise. */
static void secp256k1_fe_set_int(secp256k1_fe *r, int a);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I wondered why this accepts int. An unsigned type seems more appropriate. For type safety, we would even want to exclude too large values and use something as small as unsigned char. But I guess this is slower and just not worth the hassle)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, though this seems like something for another patch.

@elichai
Copy link
Contributor

elichai commented Jun 12, 2019

What about clearing the memory of the temporary Field Elements in the GE addition? https://github.com/bitcoin-core/secp256k1/blob/master/src/group_impl.h#L419 (and I think most of this file)

@real-or-random
Copy link
Contributor Author

real-or-random commented Jun 12, 2019

Thanks, collecting more:
https://github.com/bitcoin-core/secp256k1/blob/master/src/scalar_impl.h#L71
A lot in https://github.com/bitcoin-core/secp256k1/blob/master/src/scalar_8x32_impl.h (also also 4x64)

And I think there more in the field module... I guess I'll just go through the entire codebase. 🤷‍♂️

Copy link
Contributor

@jonasnick jonasnick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirmed by looking at the disassembly that both the memory barrier and volatile function pointer method do work with gcc 9.1.0.
Looking at the disassembly also confirmed that the memory barrier method lets gcc optimize the memset and the volatile function pointer method lets gcc actually call memset.
I didn't look at Windows.

I changed the memset overwrite from 0x00 to 0x42 and added a function

void secp256k1_do_nothing(void) {
    unsigned char foo[32];
    secp256k1_mem_clear(foo, sizeof(foo));
}

After compiling the tests with -fno-stack-protector (for brevity) the output of objdump -d tests is:

000000000001ac30 <secp256k1_do_nothing>:
   1ac30:	66 0f 6f 05 e8 2c 04 	movdqa 0x42ce8(%rip),%xmm0        # 5d920 <secp256k1_ge_const_g+0x80>
   1ac37:	00 
   1ac38:	48 8d 44 24 d8       	lea    -0x28(%rsp),%rax
   1ac3d:	0f 29 44 24 d8       	movaps %xmm0,-0x28(%rsp)
   1ac42:	0f 29 44 24 e8       	movaps %xmm0,-0x18(%rsp)
   1ac47:	c3                   	retq   
   1ac48:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
   1ac4f:	00 

This moves 16 bytes of 0x42 located at 5d920 (readelf -x .rodata tests) into xmm0 and then moves two times 16 bytes from xmm0 into the foo buffer.

With the volatile function pointer method the disassembly is as follows:

000000000001acd0 <secp256k1_do_nothing>:
   1acd0:	48 83 ec 38          	sub    $0x38,%rsp
   1acd4:	48 8b 05 ed a2 04 00 	mov    0x4a2ed(%rip),%rax        # 64fc8 <memset@GLIBC_2.2.5>
   1acdb:	ba 20 00 00 00       	mov    $0x20,%edx
   1ace0:	be 42 00 00 00       	mov    $0x42,%esi
   1ace5:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi
   1acea:	48 89 44 24 08       	mov    %rax,0x8(%rsp)
   1acef:	48 8b 44 24 08       	mov    0x8(%rsp),%rax
   1acf4:	ff d0                	callq  *%rax
   1acf6:	48 83 c4 38          	add    $0x38,%rsp
   1acfa:	c3                   	retq   
   1acfb:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

This calls memset with arguments rsp + 10, 0x42 and 32 as expected (see the calling convention).

@jonasnick
Copy link
Contributor

Results are positive and very similar to the above with clang version 8.0.0.

@elichai
Copy link
Contributor

elichai commented Jun 24, 2019

@jonasnick did you try to benchmark if this affects performance in a non negligible way?

@jonasnick
Copy link
Contributor

On my system the memory barrier and volatile function pointer method have no measurable performance difference. However, using one of the methods vs. using nothing in bench_sign appears to be 1.2% slower (41.3us vs 40.8us, averaged over 6 runs).

Copy link
Contributor

@jonasnick jonasnick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approach ACK. Very cool, someone had to do it eventually.

Clearing no secrets in pippenger is fine, not sure why I added it.
Looks like fe_clear is replaced everywhere with set_int(0) and the right memsets are replaced with the corresponding _clear function

I noticed that a few field.h comments about magnitudes are wrong (independent of this PR).
For example a field element set to 0 with fe_set_int will have magnitude 0 but can be correctly compared with secp256k1_fe_equal.

Tests run fine under valgrind, also with endo and using the volatile function pointer method. I didn't test on Windows.

Going through the codebase to look for locations where a _clear is missing can happen in another PR.

@jonasnick
Copy link
Contributor

Looks like memsetting to 0x42 instead of 0 costs about 1% :/

ecdsa_sign: min 41.3us / avg 41.5us / max 42.2us
vs.
ecdsa_sign: min 40.9us / avg 41.1us / max 41.8us

(I changed run_benchmark count argument from 10 to 500)

@sipa
Copy link
Contributor

sipa commented Jul 29, 2019

@jonasnick That sounds like a deal breaker, if true.

@real-or-random
Copy link
Contributor Author

Note that's just signing time, not verification time.

But I agree, let's stick to 0, that's just simpler.

@sipa
Copy link
Contributor

sipa commented Aug 6, 2019

Concept ACK.

There are a number of data types which end up being cleared using the generic secp256k1_mem_clear() function after this PR (including at least secp256k1_sha256 and secp256k1_rfc6979_hmac_sha256). I think it would be cleaner to provide *_clear() functions for those as well, and use those where relevant.

This code is not supposed to handle secret data.
real-or-random and others added 3 commits April 27, 2020 17:38
We rely on memset() and an __asm__ memory barrier where it's available or
on SecureZeroMemory() on Windows. The fallback implementation uses a
volatile function pointer to memset which the compiler is not clever
enough to optimize.
There are two uses of the secp256k1_fe_clear() function that are now separated
into these two functions in order to reflect the intent:

1) initializing the memory prior to being used -> converted to fe_set_int( . , 0 )
2) zeroing the memory after being used such that no sensitive data remains. ->
    remains as fe_clear()

In the latter case, 'magnitude' and 'normalized' need to be overwritten when
VERIFY is enabled.

Co-Authored-By: isle2983 <isle2983@yahoo.com>
real-or-random and others added 5 commits April 27, 2020 17:38
Co-Authored-By: isle2983 <isle2983@yahoo.com>
Co-Authored-By: Pieter Wuille <pieter.wuille@gmail.com>
All of the invocations of secp256k1mem_clear() operate on stack
memory and happen after the function is done with the memory object.
This commit replaces existing memset() invocations and also adds
secp256k1_memclear() to code locations where clearing was missing;
there is no guarantee that this commit covers all code locations
where clearing is necessary.

Co-Authored-By: isle2983 <isle2983@yahoo.com>
This gives the caller more control about whether the state should
be cleaned (= should be considered secret), which will be useful
for example for Schnorr signature verification in the future.
Moreover, it gives the caller the possibility to clean a hash struct
without finalizing it.
@real-or-random
Copy link
Contributor Author

real-or-random commented Apr 27, 2020

I rebased this and implemented @sipa's suggestion for the hash API. Now the caller is responsible to call _clean() (instead of letting _finalize) do it. I think this is actually the right thing to do.

nit: Also renamed secp256k1_mem_clear to memclear now that we have also memczero in util.h

@real-or-random
Copy link
Contributor Author

Going through the codebase to look for locations where a _clear is missing can happen in another PR.

Indeed, so this is ready for review.

For looking into more places in the future, in particular within some arithmetic function, I was wondering if "overdoing" this will introduce more stores to memory, which is not only slightly worse for security but will probably also affect performace. For example, the explicit_bzero function in Glibc has this problem:

Also, explicit_bzero only operates on RAM. If a sensitive data object never needs to have its address taken other than to call explicit_bzero, it might be stored entirely in CPU registers until the call to explicit_bzero. Then it will be copied into RAM, the copy will be erased, and the original will remain intact. Data in RAM is more likely to be exposed by a bug than data in registers, so this creates a brief window where the data is at greater risk of exposure than it would have been if the program didn’t try to erase it at all.

https://www.gnu.org/software/libc/manual/html_node/Erasing-Sensitive-Data.html

AFAIU and I've seen from looking at generated ASM this should not happen with our memory barrier approach but I'm not 100% sure and any insight will be helpful.

Note to myself: Cleaning registers is out of the scope currently but today the cited USENIX paper reminded me of a simple but expensive way to do this: Just run the operation again. (For example, run another SHA256 transform on dummy midstate and dummy input.) This will reuse and hence overwrite the registers (modulo inlining etc).

@real-or-random
Copy link
Contributor Author

Apparently stores of 64-byte zero-over-zero can be faster than stores of other values, see https://twitter.com/BRIAN_____/status/1260913021116993536. This would be a reason not so overwrite with zeros.

But I think we shouldn't have secrets that are all zeroes.This would be relevant for indistinguishability-based primitives with arbitrary inputs, e.g., encryption, but not for signing and key exchange.

@sipa
Copy link
Contributor

sipa commented May 8, 2023

Conceptually I think it makes sense to have separate secure-erase wiping and zeroing of values. It needs a big redo by now though; are you still interesting in working on this?

@real-or-random
Copy link
Contributor Author

Conceptually I think it makes sense to have separate secure-erase wiping and zeroing of values. It needs a big redo by now though; are you still interesting in working on this?

Yeah, I certainly want to come back to this, but I don't want to commit to a specific timeline. Marking as draft for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Memory zeroization improvements
5 participants