From 3e93661b6775dd28ef133e73bf61ecf645d2ea1c Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 25 Jan 2025 16:52:46 +0800 Subject: [PATCH] feat: add sha256_length_extension_attacks --- sha256_length_extension_attacks/README.md | 26 ++ sha256_length_extension_attacks/go.mod | 3 + sha256_length_extension_attacks/main.go | 200 ++++++++++++++ sha256_length_extension_attacks/sha256.go | 261 ++++++++++++++++++ .../sha256block.go | 128 +++++++++ 5 files changed, 618 insertions(+) create mode 100644 sha256_length_extension_attacks/README.md create mode 100644 sha256_length_extension_attacks/go.mod create mode 100644 sha256_length_extension_attacks/main.go create mode 100644 sha256_length_extension_attacks/sha256.go create mode 100644 sha256_length_extension_attacks/sha256block.go diff --git a/sha256_length_extension_attacks/README.md b/sha256_length_extension_attacks/README.md new file mode 100644 index 0000000..d6301a3 --- /dev/null +++ b/sha256_length_extension_attacks/README.md @@ -0,0 +1,26 @@ +> Copied from: https://github.com/skerkour/kerkour.com/tree/main/blog/2023/sha256_length_extension_attacks + +# [Breaking SHA256: length extension attacks in practice](https://kerkour.com/sha256-length-extension-attacks) + + +## Usage + +```bash +$ go run ./ -verbose +SecretKey: 7365637265747365637265747365637265747365637265747365637265747365 +Legitimate Data: user_id=1&role=user +Legitimate Signature SHA256(SecretKey || LegitimateData): 5b0b4b2472778fea87faac08a72a47d24538bff9d7f19a3a85d069893e2b08ab +Verify LegitimateSignature == SHA256(SecretKey || LegitimateData): true + +--------------------------------------------------------------------------------------------------- + +Malicious Data: &something=true&role=admin +Malicious Message (LegitimateData || padding || MaliciousData): +00000000 75 73 65 72 5f 69 64 3d 31 26 72 6f 6c 65 3d 75 |user_id=1&role=u| +00000010 73 65 72 80 00 00 00 00 00 00 00 00 00 00 01 98 |ser.............| +00000020 26 73 6f 6d 65 74 68 69 6e 67 3d 74 72 75 65 26 |&something=true&| +00000030 72 6f 6c 65 3d 61 64 6d 69 6e |role=admin| + +Malicious Signature: 8c37e11e8397b39cba72fa0e4769716c69a7ba9e29cfaf00d4601e086e85dd8f +Verify MaliciousSignature == SHA256(SecretKey, MaliciousMessage): true +``` diff --git a/sha256_length_extension_attacks/go.mod b/sha256_length_extension_attacks/go.mod new file mode 100644 index 0000000..9421bed --- /dev/null +++ b/sha256_length_extension_attacks/go.mod @@ -0,0 +1,3 @@ +module sha256_length_extension_attacks + +go 1.22 diff --git a/sha256_length_extension_attacks/main.go b/sha256_length_extension_attacks/main.go new file mode 100644 index 0000000..4bb6b2c --- /dev/null +++ b/sha256_length_extension_attacks/main.go @@ -0,0 +1,200 @@ +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "encoding/binary" + "encoding/hex" + "flag" + "fmt" +) + +var verbose bool + +func main() { + flag.BoolVar(&verbose, "verbose", false, "verbose") + flag.Parse() + + // 256 bits secretKey + // in real life it should be generated using crypto.Rand + secretKey := []byte("secretsecretsecretsecretsecretse") + + legitimateData := []byte("user_id=1&role=user") + legitimateSignature := sign(secretKey, legitimateData) + + // sha256 := append(make([]byte, 0), secretKey...) + // // sha256 := make([]byte, 0) + // sha256 = append(sha256, legitimateData...) + // oadding := generatePadding(uint64(len(secretKey)), uint64(len(legitimateData))) + // sha256 = append(sha256, oadding...) + // fmt.Println(hex.Dump(sha256)) + + fmt.Printf("SecretKey: %s\n", hex.EncodeToString(secretKey)) + fmt.Printf("Legitimate Data: %s\n", string(legitimateData)) + fmt.Printf("Legitimate Signature SHA256(SecretKey || LegitimateData): %s\n", hex.EncodeToString(legitimateSignature)) + fmt.Printf("Verify LegitimateSignature == SHA256(SecretKey || LegitimateData): %v\n", verifySignature(secretKey, legitimateSignature, legitimateData)) + + fmt.Println("\n---------------------------------------------------------------------------------------------------\n") + + maliciousData := []byte("&something=true&role=admin") + maliciousMessage := generateMaliciousMessage(uint64(len(secretKey)), legitimateData, maliciousData) + maliciousSignature := forgeSignature(legitimateSignature, maliciousData, uint64(len(secretKey)+len(legitimateData))) + + fmt.Printf("Malicious Data: %s\n", string(maliciousData)) + if verbose { + fmt.Println("Malicious Message (LegitimateData || padding || MaliciousData):") + fmt.Println(hex.Dump(maliciousMessage)) + } + fmt.Printf("Malicious Signature: %s\n", hex.EncodeToString(maliciousSignature)) + fmt.Printf("Verify MaliciousSignature == SHA256(SecretKey, MaliciousMessage): %v\n", verifySignature(secretKey, maliciousSignature, maliciousMessage)) +} + +// forgeSignature performs a length extension attack by loading a SHA256 hash from the legitimate signature +// and appending the malicious data. +func forgeSignature(legitimateSignature []byte, maliciousData []byte, secretKeyAndDataLength uint64) (forgedSignature []byte) { + digest := loadSha256(legitimateSignature, secretKeyAndDataLength) + + digest.Write(maliciousData) + hash := digest.Sum(nil) + forgedSignature = hash[:] + + return +} + +// generateMaliciousMessage generates the malicious message used to forge a signature without knowing the +// secretKey. The message has the following format: (legitimateData || padding || maliciousData) +func generateMaliciousMessage(secretKeyLength uint64, legitimateData []byte, maliciousData []byte) (message []byte) { + padding := generatePadding(secretKeyLength + uint64(len(legitimateData))) + message = make([]byte, 0, len(legitimateData)+len(padding)+len(maliciousData)) + + message = append(message, legitimateData...) + message = append(message, padding...) + message = append(message, maliciousData...) + + return +} + +// generatePadding generates the required padding to fill SHA256 blocks of 512 bits (64 bytes) +// with (secretKey || data || padding) +// The padding format is defined in RFC6234: https://www.rfc-editor.org/rfc/rfc6234#page-8 +// inspired by `sha256.go` +func generatePadding(secretKeyAndDataLength uint64) []byte { + var tmp [64 + 8]byte // padding + length buffer + var t uint64 + + // Padding. Add a 1 bit and 0 bits until 56 bytes mod 64. + tmp[0] = 0x80 + if secretKeyAndDataLength%64 < 56 { + t = 56 - secretKeyAndDataLength%64 + } else { + t = 64 + 56 - secretKeyAndDataLength%64 + } + + // Length in bits. + secretKeyAndDataLength <<= 3 + padlen := tmp[:t+8] + binary.BigEndian.PutUint64(padlen[t+0:], secretKeyAndDataLength) + + return padlen +} + +// verifySignature verifies that Signature == SHA256(secretKey || data) +func verifySignature(secretKey []byte, signatureToVerify []byte, data []byte) (isValid bool) { + isValid = false + signature := sign(secretKey, data) + + if subtle.ConstantTimeCompare(signature, signatureToVerify) == 1 { + isValid = true + } + + return +} + +// sign generates a SHA256 MAC such as SHA256(secretKey || data) +func sign(secretKey []byte, data []byte) (signature []byte) { + hasher := sha256.New() + + hasher.Write(secretKey) + hasher.Write(data) + hash := hasher.Sum(nil) + signature = hash[:] + + return +} + +// loadSha256 is a slightly modified version of digest.UnmarshalBinary in order to load the state from a +// normal SHA256 hash instead of the "proprietary version" generated by digest.MarshalBinary +func loadSha256(hashBytes []byte, secretKeyAndDataLength uint64) (hash *digest) { + if len(hashBytes) != sha256.Size { + panic("loadSha256: not a valid SHA256 hash") + } + + hash = new(digest) + hash.Reset() + + hashBytes, hash.h[0] = consumeUint32(hashBytes) + hashBytes, hash.h[1] = consumeUint32(hashBytes) + hashBytes, hash.h[2] = consumeUint32(hashBytes) + hashBytes, hash.h[3] = consumeUint32(hashBytes) + hashBytes, hash.h[4] = consumeUint32(hashBytes) + hashBytes, hash.h[5] = consumeUint32(hashBytes) + hashBytes, hash.h[6] = consumeUint32(hashBytes) + _, hash.h[7] = consumeUint32(hashBytes) + // hash.len is the nearest upper multiple of 64 of the hashed data (secretKeyAndDataLength) + // hash.len = secretKeyAndDataLength + 64 - (secretKeyAndDataLength % 64) + // hash.nx = int(hash.len % chunk) + // hash.len is the length of consumed bytes, including the paddings + hash.len = secretKeyAndDataLength + uint64(len(generatePadding(secretKeyAndDataLength))) + + return +} + +// func signBinary(secretKey []byte, data []byte) (signature []byte) { +// hasher := new(digest) +// hasher.Reset() + +// hasher.Write(secretKey) +// hasher.Write(data) +// hash := hasher.checkSum() +// signature = hash[:] + +// if verbose { +// binary, _ := hasher.MarshalBinary() +// fmt.Println("SHA256 Binary:") +// fmt.Println(hex.Dump(binary)) +// } +// return +// } + +// func loadSha256Binary(hashBytes []byte, secretKeyAndDataLength uint64) (hash *digest) { +// digestBinary := make([]byte, 0, marshaledSize) +// digestBinary = append(digestBinary, []byte(magic256)...) +// digestBinary = append(digestBinary, hashBytes...) +// digestBinary = append(digestBinary, make([]byte, chunk)...) +// digestBinary = binary.BigEndian.AppendUint64(digestBinary, secretKeyAndDataLength+64-(secretKeyAndDataLength%64)) + +// hash = new(digest) +// hash.Reset() +// err := hash.UnmarshalBinary(digestBinary) +// if err != nil { +// panic(err) +// } + +// if verbose { +// fmt.Println("SHA256 state:") +// fmt.Println(hex.Dump(digestBinary)) +// } + +// return +// } + +// dumpBinary prints 00000000 00000000 00000000 00000001 +// func dumpBinary(data []byte) { +// for i, n := range data { +// fmt.Printf("%08b ", n) +// if (i+1)%4 == 0 && i != 0 { +// fmt.Println("") +// } +// } +// fmt.Println("") +// } diff --git a/sha256_length_extension_attacks/sha256.go b/sha256_length_extension_attacks/sha256.go new file mode 100644 index 0000000..7062ef3 --- /dev/null +++ b/sha256_length_extension_attacks/sha256.go @@ -0,0 +1,261 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package sha256 implements the SHA224 and SHA256 hash algorithms as defined +// in FIPS 180-4. +package main + +import ( + "crypto" + // "crypto/internal/boring" + "encoding/binary" + "errors" + "hash" +) + +func init() { + crypto.RegisterHash(crypto.SHA224, New224) + crypto.RegisterHash(crypto.SHA256, New) +} + +// The size of a SHA256 checksum in bytes. +const Size = 32 + +// The size of a SHA224 checksum in bytes. +const Size224 = 28 + +// The blocksize of SHA256 and SHA224 in bytes. +const BlockSize = 64 + +const ( + chunk = 64 + init0 = 0x6A09E667 + init1 = 0xBB67AE85 + init2 = 0x3C6EF372 + init3 = 0xA54FF53A + init4 = 0x510E527F + init5 = 0x9B05688C + init6 = 0x1F83D9AB + init7 = 0x5BE0CD19 + init0_224 = 0xC1059ED8 + init1_224 = 0x367CD507 + init2_224 = 0x3070DD17 + init3_224 = 0xF70E5939 + init4_224 = 0xFFC00B31 + init5_224 = 0x68581511 + init6_224 = 0x64F98FA7 + init7_224 = 0xBEFA4FA4 +) + +// digest represents the partial evaluation of a checksum. +type digest struct { + h [8]uint32 + x [chunk]byte + nx int + len uint64 + is224 bool // mark if this digest is SHA-224 +} + +const ( + magic224 = "sha\x02" + magic256 = "sha\x03" + marshaledSize = len(magic256) + 8*4 + chunk + 8 +) + +func (d *digest) MarshalBinary() ([]byte, error) { + b := make([]byte, 0, marshaledSize) + if d.is224 { + b = append(b, magic224...) + } else { + b = append(b, magic256...) + } + b = binary.BigEndian.AppendUint32(b, d.h[0]) + b = binary.BigEndian.AppendUint32(b, d.h[1]) + b = binary.BigEndian.AppendUint32(b, d.h[2]) + b = binary.BigEndian.AppendUint32(b, d.h[3]) + b = binary.BigEndian.AppendUint32(b, d.h[4]) + b = binary.BigEndian.AppendUint32(b, d.h[5]) + b = binary.BigEndian.AppendUint32(b, d.h[6]) + b = binary.BigEndian.AppendUint32(b, d.h[7]) + b = append(b, d.x[:d.nx]...) + b = b[:len(b)+len(d.x)-d.nx] // already zero + b = binary.BigEndian.AppendUint64(b, d.len) + return b, nil +} + +func (d *digest) UnmarshalBinary(b []byte) error { + if len(b) < len(magic224) || (d.is224 && string(b[:len(magic224)]) != magic224) || (!d.is224 && string(b[:len(magic256)]) != magic256) { + return errors.New("crypto/sha256: invalid hash state identifier") + } + if len(b) != marshaledSize { + return errors.New("crypto/sha256: invalid hash state size") + } + b = b[len(magic224):] + b, d.h[0] = consumeUint32(b) + b, d.h[1] = consumeUint32(b) + b, d.h[2] = consumeUint32(b) + b, d.h[3] = consumeUint32(b) + b, d.h[4] = consumeUint32(b) + b, d.h[5] = consumeUint32(b) + b, d.h[6] = consumeUint32(b) + b, d.h[7] = consumeUint32(b) + b = b[copy(d.x[:], b):] + b, d.len = consumeUint64(b) + d.nx = int(d.len % chunk) + return nil +} + +func consumeUint64(b []byte) ([]byte, uint64) { + _ = b[7] + x := uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 + return b[8:], x +} + +func consumeUint32(b []byte) ([]byte, uint32) { + _ = b[3] + x := uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 + return b[4:], x +} + +func (d *digest) Reset() { + if !d.is224 { + d.h[0] = init0 + d.h[1] = init1 + d.h[2] = init2 + d.h[3] = init3 + d.h[4] = init4 + d.h[5] = init5 + d.h[6] = init6 + d.h[7] = init7 + } else { + d.h[0] = init0_224 + d.h[1] = init1_224 + d.h[2] = init2_224 + d.h[3] = init3_224 + d.h[4] = init4_224 + d.h[5] = init5_224 + d.h[6] = init6_224 + d.h[7] = init7_224 + } + d.nx = 0 + d.len = 0 +} + +// New returns a new hash.Hash computing the SHA256 checksum. The Hash +// also implements encoding.BinaryMarshaler and +// encoding.BinaryUnmarshaler to marshal and unmarshal the internal +// state of the hash. +func New() hash.Hash { + d := new(digest) + d.Reset() + return d +} + +// New224 returns a new hash.Hash computing the SHA224 checksum. +func New224() hash.Hash { + d := new(digest) + d.is224 = true + d.Reset() + return d +} + +func (d *digest) Size() int { + if !d.is224 { + return Size + } + return Size224 +} + +func (d *digest) BlockSize() int { return BlockSize } + +func (d *digest) Write(p []byte) (nn int, err error) { + nn = len(p) + d.len += uint64(nn) + if d.nx > 0 { + n := copy(d.x[d.nx:], p) + d.nx += n + if d.nx == chunk { + block(d, d.x[:]) + d.nx = 0 + } + p = p[n:] + } + if len(p) >= chunk { + n := len(p) &^ (chunk - 1) + block(d, p[:n]) + p = p[n:] + } + if len(p) > 0 { + d.nx = copy(d.x[:], p) + } + return +} + +func (d *digest) Sum(in []byte) []byte { + // Make a copy of d so that caller can keep writing and summing. + d0 := *d + hash := d0.checkSum() + if d0.is224 { + return append(in, hash[:Size224]...) + } + return append(in, hash[:]...) +} + +func (d *digest) checkSum() [Size]byte { + len := d.len + // Padding. Add a 1 bit and 0 bits until 56 bytes mod 64. + var tmp [64 + 8]byte // padding + length buffer + tmp[0] = 0x80 + var t uint64 + if len%64 < 56 { + t = 56 - len%64 + } else { + t = 64 + 56 - len%64 + } + + // Length in bits. + len <<= 3 + padlen := tmp[:t+8] + binary.BigEndian.PutUint64(padlen[t+0:], len) + d.Write(padlen) + + if d.nx != 0 { + panic("d.nx != 0") + } + + var digest [Size]byte + + binary.BigEndian.PutUint32(digest[0:], d.h[0]) + binary.BigEndian.PutUint32(digest[4:], d.h[1]) + binary.BigEndian.PutUint32(digest[8:], d.h[2]) + binary.BigEndian.PutUint32(digest[12:], d.h[3]) + binary.BigEndian.PutUint32(digest[16:], d.h[4]) + binary.BigEndian.PutUint32(digest[20:], d.h[5]) + binary.BigEndian.PutUint32(digest[24:], d.h[6]) + if !d.is224 { + binary.BigEndian.PutUint32(digest[28:], d.h[7]) + } + + return digest +} + +// Sum256 returns the SHA256 checksum of the data. +func Sum256(data []byte) [Size]byte { + var d digest + d.Reset() + d.Write(data) + return d.checkSum() +} + +// Sum224 returns the SHA224 checksum of the data. +func Sum224(data []byte) [Size224]byte { + var d digest + d.is224 = true + d.Reset() + d.Write(data) + sum := d.checkSum() + ap := (*[Size224]byte)(sum[:]) + return *ap +} diff --git a/sha256_length_extension_attacks/sha256block.go b/sha256_length_extension_attacks/sha256block.go new file mode 100644 index 0000000..9f71784 --- /dev/null +++ b/sha256_length_extension_attacks/sha256block.go @@ -0,0 +1,128 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// SHA256 block step. +// In its own file so that a faster assembly or C version +// can be substituted easily. + +package main + +import "math/bits" + +var _K = []uint32{ + 0x428a2f98, + 0x71374491, + 0xb5c0fbcf, + 0xe9b5dba5, + 0x3956c25b, + 0x59f111f1, + 0x923f82a4, + 0xab1c5ed5, + 0xd807aa98, + 0x12835b01, + 0x243185be, + 0x550c7dc3, + 0x72be5d74, + 0x80deb1fe, + 0x9bdc06a7, + 0xc19bf174, + 0xe49b69c1, + 0xefbe4786, + 0x0fc19dc6, + 0x240ca1cc, + 0x2de92c6f, + 0x4a7484aa, + 0x5cb0a9dc, + 0x76f988da, + 0x983e5152, + 0xa831c66d, + 0xb00327c8, + 0xbf597fc7, + 0xc6e00bf3, + 0xd5a79147, + 0x06ca6351, + 0x14292967, + 0x27b70a85, + 0x2e1b2138, + 0x4d2c6dfc, + 0x53380d13, + 0x650a7354, + 0x766a0abb, + 0x81c2c92e, + 0x92722c85, + 0xa2bfe8a1, + 0xa81a664b, + 0xc24b8b70, + 0xc76c51a3, + 0xd192e819, + 0xd6990624, + 0xf40e3585, + 0x106aa070, + 0x19a4c116, + 0x1e376c08, + 0x2748774c, + 0x34b0bcb5, + 0x391c0cb3, + 0x4ed8aa4a, + 0x5b9cca4f, + 0x682e6ff3, + 0x748f82ee, + 0x78a5636f, + 0x84c87814, + 0x8cc70208, + 0x90befffa, + 0xa4506ceb, + 0xbef9a3f7, + 0xc67178f2, +} + +func block(dig *digest, p []byte) { + var w [64]uint32 + h0, h1, h2, h3, h4, h5, h6, h7 := dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4], dig.h[5], dig.h[6], dig.h[7] + for len(p) >= chunk { + // Can interlace the computation of w with the + // rounds below if needed for speed. + for i := 0; i < 16; i++ { + j := i * 4 + w[i] = uint32(p[j])<<24 | uint32(p[j+1])<<16 | uint32(p[j+2])<<8 | uint32(p[j+3]) + } + for i := 16; i < 64; i++ { + v1 := w[i-2] + t1 := (bits.RotateLeft32(v1, -17)) ^ (bits.RotateLeft32(v1, -19)) ^ (v1 >> 10) + v2 := w[i-15] + t2 := (bits.RotateLeft32(v2, -7)) ^ (bits.RotateLeft32(v2, -18)) ^ (v2 >> 3) + w[i] = t1 + w[i-7] + t2 + w[i-16] + } + + a, b, c, d, e, f, g, h := h0, h1, h2, h3, h4, h5, h6, h7 + + for i := 0; i < 64; i++ { + t1 := h + ((bits.RotateLeft32(e, -6)) ^ (bits.RotateLeft32(e, -11)) ^ (bits.RotateLeft32(e, -25))) + ((e & f) ^ (^e & g)) + _K[i] + w[i] + + t2 := ((bits.RotateLeft32(a, -2)) ^ (bits.RotateLeft32(a, -13)) ^ (bits.RotateLeft32(a, -22))) + ((a & b) ^ (a & c) ^ (b & c)) + + h = g + g = f + f = e + e = d + t1 + d = c + c = b + b = a + a = t1 + t2 + } + + h0 += a + h1 += b + h2 += c + h3 += d + h4 += e + h5 += f + h6 += g + h7 += h + + p = p[chunk:] + } + + dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4], dig.h[5], dig.h[6], dig.h[7] = h0, h1, h2, h3, h4, h5, h6, h7 +}