sherlock-2025-06-symbiotic-relay-h01

[H-01] Malicious operator can alone, with any voting power smaller than quorum forge a proof

보고서

Summary

악성 오퍼레이터가 정족수보다 적은 양의 투표권으로도 증명을 위조하고 검증을 우회하여 다음 valSetHeader를 조작할 수 있다. 이를 통해 네트워크를 장악할 수 있다.

Keyword

zkp, circuit, lack of input validation, consensus, signature, cryptography, bn254, elliptic curve

Vulnerability

circuit.go 에서 참여할 밸리데이터 목록 해시를 계산할 때, 오퍼레이터의 키가 X = 0, Y = 0 이면(이하 null key) 해당 오퍼레이터를 밸리데이터 해시에 포함하지 않는다. api.Select 함수는 첫번째 인자가 true이면(즉, null key이면) 두번째 인자를 리턴하고, false이면 세번쨰 인자를 리턴한다. 참고 링크

  for i := range circuit.ValidatorData {
    hashAffineG1(&mimcApi, &circuit.ValidatorData[i].Key)
@>  mimcApi.Write(circuit.ValidatorData[i].VotingPower)
@>  valsetHashTemp := mimcApi.Sum()
 
@>  valsetHash = api.Select(
@>    api.And(fieldFpApi.IsZero(&circuit.ValidatorData[i].Key.X), fieldFpApi.IsZero(&circuit.ValidatorData[i].Key.Y)),
      valsetHash,
      valsetHashTemp,
    )
 
    // get power if NON-SIGNER otherwise 0
@>  pow := api.Select(circuit.ValidatorData[i].IsNonSigner, frontend.Variable(0), circuit.ValidatorData[i].VotingPower)
    signersAggVotingPower = api.Add(signersAggVotingPower, pow)
 
    // get key if SIGNER otherwise zero point
    point := curveApi.Select(api.IsZero(circuit.ValidatorData[i].IsNonSigner), &circuit.ValidatorData[i].Key, &sw_bn254.G1Affine{
        X: emulated.ValueOf[emulated.BN254Fp](0),
        Y: emulated.ValueOf[emulated.BN254Fp](0),
    })
    signersAggKey = curveApi.AddUnified(signersAggKey, point)
  }

null key인 오퍼레이터가 마지막에 스킵되어야 하는 이유는 valsetHash가 X, Y가 0이 아닐 때에는 valsetHashTemp를 이용하기 때문이다. null key 오퍼레이터의 투표권이 포함된 mimcApi를 이용하여 다음 valsetHashTemp를 계산하게 되므로 공격에 실패한다. 이를 피하기 위해서는 [OP1, ..., OPn, (0,0)] 와 같이 null key가 배열의 마지막에 있어야 한다.

null 키를 가진 오퍼레이터는 실제 오퍼레이터가 아니므로, 그들의 투표권은 반드시 0이어야 한다. 그러나 실제로는 이 가짜 null 오퍼레이터의 투표권을 정족수를 초과하는 임의의 값으로 설정할 수 있으며, 그럼에도 증명은 여전히 통과된다.

IsNonSigner 플래그를 false 설정하면 null 오퍼레이터의 투표권을 signersAggVotingPower에 포함하여 결과적으로 집계 키에 추가된다. 그러나 null 키의 특징은 집계 키에 추가되어도 아무런 효과가 없다는 점이다.(0을 더하므로 변화가 없다는 의미인 듯) 이는 실질적으로 null 키로의 서명이 필요하지 않음을 의미한다. 따라서 검증자 집합 [OP1, ..., OPn, (0,0)]에서 검증자 1부터 n까지의 서명이 있다면 서명 검증을 통과하게 된다.

이는 실질적으로 최소한의 투표 권한을 가진 어떤 오퍼레이터라도 이 null 오퍼레이터를 추가하여 정족수를 초과하는 투표 권한을 부여하고 메시지를 통과시킬 수 있음을 의미한다.

Impact

공격자는 데이터를 조작하고 네트워크, 더 정확히 말하면 Settlement.sol의 다음 에포크에 대한 valSetHeader를 완전히 통제하여 네트워크를 완전히 침해할 수 있다.

Mitigation

null Key의 투표권은 0으로 강제한다.

Memo

이 컨테스트에서는 ZK 관련 코드도 오딧 스코프에 포함하긴 했지만, 그 비중을 줄였다. ZK 관련 코드에서 나온 취약점은 점수를 줄이고 따로 취급하였다. 그래서 이 취약점은 따로 분류되었지만 여기서는 심각도에 따라 High로 기록했다.

재밌는 취약점이다. 암호의 특성을 알고 있어야 찾을 수 있다.


tags: bughunting, symbiotic, smart contract, go lang, restaking, severity high, zkp, circuit, lack-of-input-validation-vul, consensus, signature, cryptography, bn254, elliptic curve