Salamander finalmask: Replace math/rand with crypto/rand in salt generation#6228
Salamander finalmask: Replace math/rand with crypto/rand in salt generation#6228IconHHw wants to merge 3 commits into
math/rand with crypto/rand in salt generation#6228Conversation
|
AI很喜欢写 rand.NewSource(time.Now().UnixNano()) 这种pattern |
|
原mathrand即使加了种子也是伪随机, |
|
math/rand 基于 Lagged Fibonacci generator,即使换成真随机种子,攻击者可以大胆假设目标就是基于 math/rand 的 Salamander,收集到足够多(假如无丢包或乱序是607个)连续 Salt 后,用607个连续Salt(每个8字节 Salt,恰好对应一次 Uint64() 调用),恢复出整个环形缓冲区作为当前状态,然后尝试预测后续所有 Salt,如成功预测则推断为 Salamander |
|
难绷 |
|
Sorry,我之前的描述不太准确,更正一下:Uint64()调用的结果会和 rngMask = (1 << 63) - 1 进行与运算,得到Int63()的调用结果(一个非负int64)用于填充Salt,填充时Salt只用到了其中的低56位,最高字节会被抛弃,也就是每7个Salt调用了8次Uint64(),而不是每个8字节 Salt 恰好对应一次 Uint64() 调用,攻击时需要考虑额外对齐问题 我让AI帮忙写了一个PoC(只取最初532个Salt,不考虑对齐问题)package main
import (
cRand "crypto/rand"
"encoding/binary"
"fmt"
"math/rand"
)
const (
rngLen = 607
rngTap = 273
)
func main() {
seedBytes := make([]byte, 8)
cRand.Read(seedBytes)
seed := int64(binary.BigEndian.Uint64(seedBytes))
// ── Phase A: collect salts ───────────────────────────────────────
// Use multiple-of-7 count so readPos aligns to 0, simplifying analysis.
const numSalts = 532 // = 76 * 7
src := rand.NewSource(seed)
r := rand.New(src)
salts := make([][8]byte, numSalts)
for i := range salts {
_, _ = r.Read(salts[i][:])
}
fmt.Printf("\nPhase A: Collected %d Read(8) salts (= %d × 7-salt cycles)\n",
numSalts, numSalts/7)
// ── Phase B: extract low 56 bits of underlying Uint64s ───────────
// Read(8) byte layout (7-salt cycle, consumes exactly 8 Uint64s):
// Salt 0: U[0].b[0..6] + U[1].b[0]
// Salt 1: U[1].b[1..6] + U[2].b[0..1]
// Salt 2: U[2].b[2..6] + U[3].b[0..2]
// Salt 3: U[3].b[3..6] + U[4].b[0..3]
// Salt 4: U[4].b[4..6] + U[5].b[0..4]
// Salt 5: U[5].b[5..6] + U[6].b[0..5]
// Salt 6: U[6].b[6] + U[7].b[0..6]
//
// Int63 = Uint64 & 0x7FFFFFFFFFFFFFFF. 7 bytes output per Int63 = 56 bits.
// Bits 56–62 of Uint64 are NEVER output. Bit 63 is always 0.
low56, numU64 := extractLow56(salts)
fmt.Printf("Phase B: Extracted low 56 bits of %d Uint64 values\n", numU64)
fmt.Printf(" (7 bits lost per Uint64: bits 56–62, bit 63 always 0)\n")
// Verify low56 self-consistency via LFG recurrence:
// low56[k] = (low56[k-607] + low56[k-273]) mod 2^56 for k >= 607
mismatches := 0
for k := rngLen; k < numU64; k++ {
expected := (low56[k-rngLen] + low56[k-rngTap]) & 0xFFFFFFFFFFFFFF
if low56[k] != expected {
mismatches++
}
}
fmt.Printf(" LFG consistency check: %d/%d mismatches %s\n",
mismatches, numU64-rngLen, check(mismatches == 0))
// ── Phase C: reconstruct state from low56 alone ──────────────────
// Set high 7 bits to 0. The low56 values form a CLOSED system:
// future low56 depend ONLY on known low56 values, never on high7.
// High7 values are temporarily in rand.readVal between Read calls
// but are always discarded when readPos wraps to 0.
//
// vec reconstruction: vec[i] = Uint64[(333-i+607)%607]
// We only have low56, so: vec[i] = low56[(333-i+607)%607]
low56 = low56[numU64-rngLen:] // keep only the last 607 low56 values
var vec [rngLen]int64
for i := range vec {
vec[i] = int64(low56[(333-i+rngLen)%rngLen])
}
feed, tap := 334, 0
// After 532 = 76×7 salts, the Read state is aligned: readPos = 0.
// readVal is stale but pos=0 means the next byte triggers a fresh Int63.
var rv uint64 = 0
var rp uint8 = 0
// Create a fresh reference generator at same position.
r2 := rand.New(rand.NewSource(seed))
for i := 0; i < numSalts; i++ {
var tmp [8]byte
_, _ = r2.Read(tmp[:])
}
// ── Phase D: predict future salts ────────────────────────────────
fmt.Println("\nPhase D: Predict next 10 Read(8) salts using low56-only state:")
for i := 0; i < 10; i++ {
var predicted [8]byte
simulateRead8(&vec, &feed, &tap, &rv, &rp, predicted[:])
var actual [8]byte
_, _ = r2.Read(actual[:])
ok := predicted == actual
fmt.Printf(" Salt #%d: predicted=%x actual=%x %s\n",
i, predicted, actual, check(ok))
}
}
// ── Low-56-bit extraction from Read(8) salts ────────────────────────────
// extractLow56 reconstructs the low 56 bits of each underlying Uint64 from
// a sequence of Read(8) salts using the known 7-salt/8-Uint64 byte layout.
func extractLow56(salts [][8]byte) ([]uint64, int) {
numUint64s := len(salts) * 8 / 7
low56 := make([]uint64, numUint64s)
for block := 0; block+7 <= len(salts); block += 7 {
u := block * 8 / 7
s := salts[block:]
// Salt 0: bytes 0..6 from U[u], byte 7 from U[u+1] byte 0
for j := 0; j < 7; j++ {
low56[u] |= uint64(s[0][j]) << (j * 8)
}
low56[u+1] |= uint64(s[0][7]) // byte 0 of U[u+1]
// Salt 1: bytes 0..5 from U[u+1] bytes 1..6, bytes 6..7 from U[u+2] bytes 0..1
for j := 0; j < 6; j++ {
low56[u+1] |= uint64(s[1][j]) << ((j + 1) * 8)
}
low56[u+2] |= uint64(s[1][6]) | uint64(s[1][7])<<8
// Salt 2: bytes 0..4 from U[u+2] bytes 2..6, bytes 5..7 from U[u+3] bytes 0..2
for j := 0; j < 5; j++ {
low56[u+2] |= uint64(s[2][j]) << ((j + 2) * 8)
}
low56[u+3] |= uint64(s[2][5]) | uint64(s[2][6])<<8 | uint64(s[2][7])<<16
// Salt 3: bytes 0..3 from U[u+3] bytes 3..6, bytes 4..7 from U[u+4] bytes 0..3
for j := 0; j < 4; j++ {
low56[u+3] |= uint64(s[3][j]) << ((j + 3) * 8)
}
low56[u+4] |= uint64(s[3][4]) | uint64(s[3][5])<<8 |
uint64(s[3][6])<<16 | uint64(s[3][7])<<24
// Salt 4: bytes 0..2 from U[u+4] bytes 4..6, bytes 3..7 from U[u+5] bytes 0..4
for j := 0; j < 3; j++ {
low56[u+4] |= uint64(s[4][j]) << ((j + 4) * 8)
}
for j := 0; j < 5; j++ {
low56[u+5] |= uint64(s[4][j+3]) << (j * 8)
}
// Salt 5: bytes 0..1 from U[u+5] bytes 5..6, bytes 2..7 from U[u+6] bytes 0..5
for j := 0; j < 2; j++ {
low56[u+5] |= uint64(s[5][j]) << ((j + 5) * 8)
}
for j := 0; j < 6; j++ {
low56[u+6] |= uint64(s[5][j+2]) << (j * 8)
}
// Salt 6: byte 0 from U[u+6] byte 6, bytes 1..7 from U[u+7] bytes 0..6
low56[u+6] |= uint64(s[6][0]) << (6 * 8)
for j := 0; j < 7; j++ {
low56[u+7] |= uint64(s[6][j+1]) << (j * 8)
}
}
return low56, numUint64s
}
// ── LFG core ────────────────────────────────────────────────────────────
func stepUint64(vec *[rngLen]int64, feed, tap *int) uint64 {
*tap--
if *tap < 0 {
*tap += rngLen
}
*feed--
if *feed < 0 {
*feed += rngLen
}
v := uint64(vec[*feed]) + uint64(vec[*tap])
vec[*feed] = int64(v)
return v
}
// simulateRead8 exactly replicates Rand.Read(8) on the given LFG state.
// readVal and readPos persist across calls (same as Rand.readVal / Rand.readPos).
//
// Even though our vec stores only low56 (high7=0), the simulation is exact
// because Read output bytes come from Int63 low 56 bits. The high7 spill
// in readVal is always discarded when readPos wraps to 0.
func simulateRead8(vec *[rngLen]int64, feed, tap *int, readVal *uint64, readPos *uint8, out []byte) {
for b := 0; b < len(out); b++ {
if *readPos == 0 {
*readVal = stepUint64(vec, feed, tap) & 0x7FFFFFFFFFFFFFFF
*readPos = 7
}
out[b] = byte(*readVal)
*readVal >>= 8
*readPos--
}
}
// ── Display helpers ─────────────────────────────────────────────────────
func check(ok bool) string {
if ok {
return "✓"
}
return "✗ FAIL"
}
func repeat(c byte, n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = c
}
return b
} |
|
@IconHHw 修一下 fmt |
好的,已修改 |
|
看了下还有很多 math/rand,比如 Sudoku 你看看, |
|
transport\internet\finalmask\sudoku\table.go:需要根据预共享密钥,与对端生成一致的随机序列,不宜修改; transport\internet\finalmask\sudoku\codec.go:随机填充用到了math/rand,安全起见我改成math/rand/v2了; common\utils\browser.go:该方法用 CPU 信息(型号、核心数、缓存行等)的 FNV hash 作为种子为每台机器确定性地生成相同的随机序列,似乎保持现状比较合适; common\dice\dice.go、common\session\session.go、proxy\hysteria\client.go、transport\internet\finalmask\xicmp\client.go、transport\internet\hysteria\config.go、transport\internet\hysteria\congestion\bbr\bbr_sender.go、transport\internet\hysteria\dialer.go、transport\internet\hysteria\udphop\conn.go、transport\internet\splithttp\dialer.go:Go 1.22+以后,math/rand的包级函数已经默认使用了ChaCha8算法,似乎不太需要额外修改 |
使用密码学安全的随机数生成器生成明文Salt