SHA-3 Buffer Overflow
mouha.be
mouha.be
Slightly concerning news for the (mostly-Golang-based) Ethereum ecosystem, which relies on SHA3-256 for pretty much everything...
func main() {
h := sha3.New224()
buf := make([]byte, 4294967295)
h.Write(buf)
sum := h.Sum(nil)
fmt.Printf("%x\n", sum)
}
Doesn't crash on my amd64 dev machine.Later
I could have just looked at the code, too: the assembly you've linked to is just the Keccak permutation, not the entire Go hash; the buffer management is done in Go, not in assembly.
Presumably this isn't not-crashing just because the developers of the Golang stdlib somehow found+fixed this bug back in 2015 when this assembler file was baked. The error is in that assembler code, I'm sure. It's presumably getting masked by something. (Something that may be benign, or might be subtly corrupting the runtime.)
For a benign case: maybe the Golang runtime isn't allocating you precisely as many bytes as you're asking for, but rather a little bit more? Perhaps rounding up to a multiple of a page for more-than-page-sized allocations?
Not having access to an amd64 machine at the moment, I'll have to ask you: does increasing the size by one, as in the article, cause an infinite loop?
c5bcc3bc73b5ef45e91d2d7c70b64f196fac08eee4e4acf6e6571ebe
No matter what Go is allocating under the hood, it has to be feeding exactly that many bytes to the SHA3 algorithm.Well, yes, but this is supposed to be a buffer overflow — the algorithm itself is reading and/or writing past the end of the buffer it's been handed. My hypothesis was that Golang is allocating in such a way that reading/writing a single byte past the received slice bounds won't result in a protection fault in the way you'd expect if the allocation were exact.
https://github.com/golang/crypto/blob/642fcc37f5043eadb2509c...
Possibly not as fast as C, but easier to reason about, I'd say.
A second later
Oh, wait, yeah, this is just the Keccak permutation in assembly, not the entire hash. That was dumb of me. Yeah, this code looks ok?
The outer loop of the Write method (function?) is the same for all architectures.
One sec and I'll get you an answer to your thing about the extra write.
c5bcc3bc73b5ef45e91d2d7c70b64f196fac08eee4e4acf6e6571ebe
With the extra write and an extra byte on the big write (to try to trigger the loop condition): ec66be1ebccf055f839fccf2d12e641dcbbda4f5c71a3bdee6509495Read it yourself here:
https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.1.0:...
There's obviously no problem.
The point of NIST standardizing on SHA-3 is to gradually replace SHA-2 due to the rise of computing power and the likelihood it will become as weak as SHA-1 is now in the near future. Unfortunately, like American credit cards vs. European chip & pin, it's going to take forever to adopt.
So we need to be careful not to use it in setting where that would be problematic. Or we can use it with workarounds, like double hashing (SHA-256d) or truncating its output.
SHA-3 is sponge based, so its output is always truncated, preventing length-extension attacks. So I think SHA-3 is a better default, though it's fine to use SHA-2 if you know what you're doing.
In a similar manner, apps that verify signatures/certificates often let the certificate specify which algorithm is used, so again effectively an attacker may be able to force the app to use SHA3 as long as it uses a library that has some support for it.
This is a common problem, and massively increases attack surface, since if a vulnerability can be found in any algorithm, it can be exploited. Downgrade attacks are a varient of the same.
Let me present an alternative:
Each protocol has one specific encryption algorithm and set of parameters. However, that set isn't fixed, but defined based on the current date and time.
For example, HTTPS could be defined as "SHA1 from 1996 till 2006", "SHA256 from 2005 till 2018", "SHA3 from 2017 till 2032", etc.
The dates can be written 10+ years into the future, so that software deployed today is still usable that many years into the future. As soon as the last date period expires, the software is not usable until it has been upgraded (unless both ends of the communication link choose to fake the date).
There is precedent for software 'expiring' - that happens whenever the root certificate of a trust store reaches an expiry date.
The downside is sometimes an algorithm might be broken before its date comes, and other times an algorithm may still be secure at that date. But that still seems better than our current mess of downgrade attacks.
Then the release of a new configuration can be done slowly with reasonable caution to get back to the desired redundancy level.
Not necessarily. If the client contacts the server using version X, the server will reply with version X.
Who says that? Either it is specified in the protocol, and then you're right that this is still a negotiation, or it is not specified in the protocol, so if the client does that is implementing some out of spec behavior. Which is true that already happened on the Internet for compatibility's sake, but keep in mind that there is still an advantage, as an adversary can't easily mess in the connection setup phase in a way that protocol version X looks like protocol version Y.
* you expect all servers to upgrade immediately, or at least faster than clients, or
* you delay upgrading clients you know to be insecure until servers had time to upgrade, or
* you are ok with breaking a significant portion of the network for every protocol update.
Either way, your proposal makes no sense for the internet.
Additionally, this is already possible with the current negotiation scheme. You can have clients refuse old algorithms (and they do). Your proposal doesn't improve anything there or anywhere else.
The main differences between the older SHA-256 of the SHA-2 family of FIPS 180, and the newer SHA3-256 of the SHA-3 family of FIPS 202, are:
* Resistance to length extension attacks.
* Performance. The SHA-2 functions—particularly SHA-512, SHA-512/224, and SHA-512/256—generally have higher performance than the SHA-3 functions. Partly this was out of paranoia and political reasons in the SHA-3 design process.
Further reading: https://crypto.stackexchange.com/questions/68307/what-is-the...
Power10 also supports accelerated SHA-3: https://www.redbooks.ibm.com/redpapers/pdfs/redp5649.pdf (p150)
Accelerated SHA-3 on x86_64 is probably an inevitability; the question is more when than if.
https://csrc.nist.gov/projects/cryptographic-algorithm-valid...
Moreover, the laptop variants of Alder Lake (H-series, P-series and U-series) are said in the Intel Ark web site to include a subset of QuickAssist, but I have not been able to find any public Intel document explaining which functions are supported by laptop Alder Lake CPUs or any report of someone testing these functions on laptop Alder Lake CPUs.
just read the link you provided: it's sha-224 and above.
i wonder if a similar issue exists in the sha-224+ padding code.
Length extension works because the hash is the entire internal hash function state. This is not true for SHA-224, SHA-384, SHA-512/224 or SHA-512/256 since for these four functions not all of the internal state is exposed.
No, the length is not included at the start of the data to be hashed, this would mean committing to entire input before we hash it, which is a pretty unsatisfactory API, the length is appended (with the padding).
that sounds right. been almost a decade since i've dug into sha-2.
i do remember the lengths in sha-256 and sha-512 though and i remember not being a huge fan of the chunking/padding code.
come to think of it, there's another reason for the message length in the padding. i forget what it is now.
With the added benefit of better performance of SHA-512 (on 64 bit systems) [2], there's no good reason to use SHA-256 rather than SHA-512/256 for new cryptographic designs.
[1] https://en.wikipedia.org/wiki/Length_extension_attack
[2] https://crypto.stackexchange.com/questions/26336/sha-512-fas...
Many modern CPUs, e.g. all AMD Zen, most Intel Atom, Intel Ice Lake and newer, most 64-bit ARM, have hardware implementations of SHA-256, which are much faster than software computing SHA-512.
Only some more recent 64-bit ARM CPUs also have hardware for SHA-512 and SHA-3.
Whenever the speed matters, SHA-256 is the best choice, unless you choose different hash algorithms based on the detected CPU.
On old 64-bit CPUs without SHA hardware, e.g. Intel Skylake, SHA-512 is faster than SHA-256 (Intel introduced SHA in Atom CPUs many years before also introducing it in Core CPUs, so that Atom could compete in Geekbench scores with the ARM CPUs, which already had SHA).
Nevertheless, most existing Xeon CPUs are too old to have SHA hardware.
In Intel server CPUs SHA was introduced many years later than in AMD server CPUs or Intel desktop CPUs, i.e. only in "the 3rd generation Xeon Scalable" based on the Ice Lake Server cores, in Q2 of 2021 (Xeon model numbers 83xx, 63xx and 53xx).
Reasonable assumption?
At that time, on x86 CPUs (I have not tested on ARM) neither sha256sum nor sha224sum nor sha1sum used the hardware instructions (and I have compiled them from sources, with the appropriate flags).
Because I have a Zen CPU (but that would also be true on Atom CPUs or Core CPUs since Ice Lake), I have to use "openssl dgst -r -sha256" instead of sha256sum and "openssl dgst -r -sha1" instead of sha1sum.
The openssl implementation uses the hardware instructions where available and in that case it is several times faster.
(The "-r" option flag in "openssl dgst" selects an output format that is more similar to the sha???sum tools, i.e. hash value followed by file name, but openssl still outputs an extra character, which must be accounted in scripts.)
Thanks for the info! Fortunately for my purposes of integrity checking, slow is okay.
On the other hand, on older Skylake/Kaby Lake/Coffee Lake etc. CPUs, both programs have the same speed.
Ouch
Edit: in case the solution I propose isn't obvious, organizations like red hatand google need to pay for these pipelines and unit tests. Getting good unit test coverage is expensive and ubsan and asan don't work without that coverage. So since no one has mentioned it yet, you could rewrite this stuff in rust as a poor man's substitute. It will catch some of the aame things, but ultimately there is no substitute for test coverage with sanitizers. Well, maybe formal analysis?
That's backwards. You'll catch more cases with a Rust-style type system that naturally checks everything, than with sanitisers that can only check the paths that get executed in tests.
It absolutely is, and this kind of absolutism is what holds back the adoption of things like rust.
In most real world software development, the target defect rate is not zero. The value proposition of something like rust for most businesses isn't that it lets you lower your defect rate; it's that it lets you maintain your existing (acceptable) defect rate at a much lower cost, by letting you drop your static analysis and most of your tests (reducing maintenance burdens) and still have a better bottom-line defect rate.
What does that even mean?
> I will bet the farm your defect rate will remain the same without any actual validation of what people write.
In what sense is a typechecker (or the rust borrow checker) not "actual validation", but a static analyser is?
> Plenty of errors due to language quirks but lots of times I see missing statements, fixed values which should be variable, input checking failures due to unknown input, incorrect assumptions and lots of non language gelated bugs
Most of those sound like type errors to me. Errors where the program isn't doing what the programmer thought it should can generally be avoided by using more precise types. (The more insidious case is where the program is doing exactly what the programmer thought it should, but they've misunderstood the specification or not thought through the implications - but no kind of testing can catch that kind of bug).
Guessing the rationale is that if you read a file, you also want to use the content for something else, not only compute its hash.
Edit: Turns out there was another comment like this, and a response was that the file could be mmap'd.
* Which is usually much harder than just free mem. I recall macOS always being able to give a huge chunk of memory, I guess by rearranging on the fly, but it's very slow if you ask for a lot. And Linux just says no.
Linux might say yes, even if the memory isn't available. See notes of malloc:
"This means that when malloc() returns non-NULL there is no guarantee that the memory really is available." [0]
gem specific_install https://github.com/steakknife/digest-sha3-ruby
git version 2.31.1
http installing from https://github.com/steakknife/digest-sha3-ruby
Cloning into '/var/folders/x6/87j2gpl54x79m0nvns2lsrpc0000gn/T/d20221020-79527-r1vite'...
remote: Enumerating objects: 191, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 191 (delta 0), reused 0 (delta 0), pack-reused 188
Receiving objects: 100% (191/191), 6.10 MiB | 11.79 MiB/s, done.
Resolving deltas: 100% (65/65), done.
Successfully built RubyGem
Name: digest-sha3
Version: 2.0.0
File: digest-sha3-2.0.0.gem
Building native extensions. This could take a while...
Successfully installed
pry -rdigest/sha3
[1] pry(main)> h = Digest::SHA3.new(224)
=> #<Digest::SHA3: 6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7>
[2] pry(main)> h.update("\x00")
=> #<Digest::SHA3: bdd5167212d2dc69665f5a8875ab87f23d5ce7849132f56371a19096>
[3] pry(main)> h.update("\x00"*4294967295)
=> #<Digest::SHA3: c5bcc3bc73b5ef45e91d2d7c70b64f196fac08eee4e4acf6e6571ebe>
[4] pry(main)> h.hexdigest
=> "c5bcc3bc73b5ef45e91d2d7c70b64f196fac08eee4e4acf6e6571ebe"
[5] pry(main)> $ gem install sha3
$ irb
> require 'sha3'
> s = SHA3::Digest::SHA224.new
> s.update("\x00")
> s.update("\x00" * 4294967295)
[ Segmentation fault... ]
Tested with Ruby 3.1.2Gem's code (including C native extension): https://github.com/johanns/sha3
The vulnerability is only present in the reference implementation. So it's unlikely that other implementations (rush as Rust or Go) are vulnerable.
I tested with the latest released and master branches of PHP, both of which segfaulted for the code samples mentioned in the article. The article says Python is also vulnerable.this is because both of the languages apparently use the reference implementation.
Ethereum uses SHA3 quite extensively, but I doubt it's vulnerable post migration from PoW, let alone it's unlikely that they use the vulnerable implementation/variant.
Most web servers default to a 50MB limit or something like that, I think, not many things accept by default the size required for this exploit. Most cases would involve services that accept large untrusted files from users.
1. Break the file into multiple chunks and pass them to SHA-3 individually.
2. Make one of those chunks larger than 4 GB in size. (This requires using 4 GB of memory.)
This is kind of an unlikely thing for an application to do. If you're breaking the file into chunks, usually you'd use smaller chunks. (And if your server is limited to less than 4 GB of RAM, you might not be able to allocate enough memory to trigger the vulnerability in any case.) But it could be exploitable in some really weird or poorly written application somewhere.
(mmap() can be reasonably used for streaming usecases like this provided you use madvise() hints on whether you want the data kept resident after use, probably non unix platforms have similar apis)
I still think it's unlikely for an app to be vulnerable to this but having less than 4 GB of RAM isn't an absolute defense. Having a 32-bit address space probably is, though, if you're running on old or embedded hardware.
I’ve shown how this vulnerability in XKCP can be used to violate the cryptographic properties of the hash function to create preimages, second preimages, and collisions. Moreover, I’ve also shown how a specially constructed file can result in arbitrary code execution, and the vulnerability can also impact signature verification algorithms such as Ed448 that require the use of SHA-3. The details of these attacks will be made public at a later date.
a specially constructed file can result in arbitrary code executionOuch, thats not looking good for a reference implementation for a piece of software deployed on billions and billions of machines that was written by experts and reviewed (and modified) by even bigger egg heads.
C, f**ing C. By the sake of god. Not C++.
Stop to put both in the same basket, it just show you do not know what you are talking about.
https://github.com/XKCP/XKCP/commit/fdc6fef075f4e81d6b1bc383...
Something like that in modern C++ would have been done using std::span<> and that prevents this kind of out-of-bound access and the party-time that comes with it.
It is regrettable and under fix but:
- You generally do have your own implementation with more advance bound check for safety-critical implementation. - We do have our own where I work right now. - Google has its own in abseil.
- span allows the usage for range-for loop which combined with subspan() makes possible to avoid pointer arithmetic all together.
How much time did it took you to understand all this? What chances does a beginner has to understand these subtle differences and learn the particular way your codebase uses std::span and not footgun themselves? How much productivity is lost to the sheer terror of such footguns - not actual bugs, but time and effort lost that did not move the product forward?
And most importantly, what is the point of accepting that huge cost instead of writing in a language where unsafe data access is impossible?
Because the world is a place where billions of lines of code have been written. And these will never be rewritten, but can be modernized.
That the reality, and that's why your web browser right now and most of your OS is still written in unsafe language right now. And will still be for the next 15years at least.
The beast will die in the same sense Fortran or Cobol are dead: an outdated language that has no noticeable strengths in the modern world, used by a bunch of (difficult to hire) old timers to fix legacy systems.
Or you use the evolutionary approach: remove the unsafe part from C/C++ bit by bit. And do code migration (Carbon style) when you can do it. This is what some C and C++ committee members try to do and fortunately they are there.
Because that is way more likely to happen in a reasonable time frame (next 15 years) than rewrite Chrome, Linux, OpenSSL, Office and all the other 10 of billions of proprietary code we have around in Rust.
You cannot do that without breaking backward compatibility. If you maintain compatibility, what results is a sprawling meta-language with a large learning burden that has the theoretical ability to emulate a safe language, and which in practice will be used to mix both safe and unsafe idioms depending on the experience and discipline of the programmer, in an unholy mess that no static analyzer can prove as correct and no programmer can say they really fully understand.
Maybe I'm mistaken, I've last programmed in C++ a good number of years ago, but I have not encountered a (C++ programming paradigm, static linter) doublet that can prove data integrity and thread safety to an extent even close to the Rust compiler. And it would be useless for existing codebases anyhow.
So for example in Rust you have a native slice type reflecting a dynamically sized view into a contiguous sequence, and of course it's safe -- five[20] will either refuse to compile if the compiler can see this slice is too short, or it will panic at runtime. In C++ as others have mentioned actually std::span just isn't safe when used ergonomically. five[20] in C++ is Undefined Behaviour. std::optional has a safe interface for a Maybe / Option type, but is presented with a more ergonomic unsafe behaviour and that's what people use.
This is not an old-fashioned thing the C++ Committee grew out of, std::expected is in C++ 23, that's a Result type and it likewise offers an ergonomic unsafe API. The safe APIs were seen as a nice-to-have which could miss the train because who needs safety anyway? Just don't make any mistakes.
Naturally we have now ways to enable them on all major compilers, but many still don't.
This is one point I kind of agree with you.
The crucial trick is providing the unchecked operations (with improved performance) as unergonomic alternatives. *(five.get_unchecked_mut(20)) = k; // looks horrible. Nobody wants to write that, so when they don't need it they won't write it. The fact calling get_unchecked_mut requires unsafe is part of how Rust could achieve its goals, but the choice to not make this ergonomic is why it actually delivers in practice.
What CppFront wants here, and P2687 proposes, and lots of other C++ work has suggested, is roughly:
[[suppress(bounds_check)]] { five[20] = k; }
Thus imitating what they think Rust does here, rather than what it actually did, and in the process completely missing the point.I can't speak for clang, but in what concerns VC++ is mostly useless still, unless one wants to annotate everything with those kind of annotations + SAL, and even then it is only half way there.
Which is not really inspiring.
ISO on their wisdom has turned the safety defaults around in regards to classical C++ frameworks.
So C++ secure code, either has to make use of Microsoft's gsl::span<>, or again turn on bounds checking in release builds, you cannot trust everyone to call .at().
This isn't an attempt at a scary accusation, but as a pedant, this got me.
For those wondering, here is an explanation by a commenter:
The padding change is the only difference, this allows future tree hashing modes as well as the current SHAKE outputs to generate different digests given the same security parameters and message inputs. Up to 4 additional bits are added, which keeps the full padding inside a byte boundary, making implementations with octet only input able to switch to SHA-3 from Keccak with change to only a single line of code.
https://crypto.stackexchange.com/questions/10645/are-nists-c...https://cdt.org/insights/what-the-heck-is-going-on-with-nist...
ketccak team's response: https://keccak.team/2013/yes_this_is_keccak.html
You're just a baby. There's time.
Don't you have bug bounties to find and fix these things?
But this was in a public standard that we were using, we don't cover those.
Do I look like I care?
IMHO, sha3 should be part of that bounty.
Like NSO Group hacking everyone for the next 5 years deal?
It would have already been a big deal for the past five years, if anything.
Furthermore you have to induce a bufferoverflow first. I’m no security researcher, but to my mind that means you’re either (a) trusting idiots to write sensitive code without a IT Sec team reviewing it, or (b) you’ve got a malicious actor on the inside, who can slip it past review. I’m sure there are plenty of other scenarios, but I’m saying it doesn’t feel likely by gut.
(safe) Rust writes actual bounds checks, so if you accidentally overflow a buffer in some case you never tested that compiles, it would just panic if the case you got wrong occurs in real life.
A more specialised language like WUFFS doesn't write bounds checks, it just constrains all the buffer access variables, so when you write code that could overflow that doesn't compile, preventing this problem.
There is a price for this, you can have code which you know, intellectually, never hits the case where say k = 5, but maybe the proof would be sixty pages of difficult maths, WUFFS just won't compile that code until you add handling for k = 5, too bad, safe Rust insists on behaving as though k might be 5 (e.g. inserting runtime checks), unsafe Rust would allow you to YOLO, with a lot of hoop jumping, C++ doesn't care. Of course if your sixty page proof is wrong then these outcomes feel very different...
If someone used ffi, linked to it, or attempted to reproduce the behavior verbatim in unsafe code, obviously it would have the same problems as the native code.
I don't think any of those are published as libraries usable by other languages, but doing that would be less than a hundred lines for defining a C interface.