Developing @Artela_Network | Interchain Builders Program @interchain_io | ex-research @AT_Capital2021 | CMU & PKU
Developing @Artela_Network | Interchain Builders Program @interchain_io | ex-research @AT_Capital2021 | CMU & PKU

Subscribe to Mike Ma

Subscribe to Mike Ma
<100 subscribers
<100 subscribers
Share Dialog
Share Dialog


原文链接:
重入攻击仍然是一个挑战。现有的风险控制措施主要集中在协议源代码层面,并且仅在合约进入runtime状态前生效。
运行时保护是DeFi安全的重要补充,确保协议的执行与其预期设计一致。
EVM设计不支持运行时保护,因为智能合约无法访问runtime状态全部上下文信息。
Aspect Programming可以实现运行时保护,消除重入攻击。
我们逐步展示了如何通过Aspect防范对Curve合约的重入攻击。
尽管重入攻击是一个众所周知的问题,并且出现了许多风险控制措施,但在过去的两年中,涉及此类攻击的安全事件仍在不断发生:
Curve Finance攻击(2023年7月) - 6000万美元,Curve因其合约编程语言Vyper编译缺陷遭受重入攻击。
Origin Protocol攻击(2022年11月) - 700万美元,稳定币项目Origin Dollar(OUSD)遭受了重入攻击。
Siren Protocol攻击(2021年9月) - 350万美元,AMM池遭受重入攻击。
Cream Finance攻击(2021年8月) - 1880万美元,攻击者利用重入漏洞进行二次借贷。
目前,防范重入攻击的重点集中在智能合约的源代码层面,措施包括集成OpenZeppelin的ReentrancyGuard,以及对合约逻辑代码进行安全审计,以避免预定义的安全隐患。
这种方法被称为“白盒”解决方案,旨在在源代码层面规避漏洞,以最小化逻辑错误。然而,其主要挑战在于无法防御未知隐患。
将合约从源代码“转化”为实际runtime是个具有挑战性的过程。每一步可能为开发人员带来无法预料的问题,而合约源代码本身可能无法全面涵盖所有潜在情况。在Curve的案例中,由于编译器问题,即使协议源代码是正确的,最终运行结果与协议的预期设计之间仍可能存在差异。

仅仅依靠协议在源代码和编译层面的安全性是不足够的。即使源代码看起来毫无瑕疵,由于编译器问题,漏洞仍可能意外出现。
与现有的风险控制措施集中在协议源代码层面并在运行之前生效不同,运行时保护涉及协议开发人员编写运行时保护规则和操作,以处理运行时的未预料情况。 这有助于对运行时执行结果进行实时评估及应对。

运行时保护在增强DeFi安全性方面至关重要,是现有安全措施的重要补充。通过以“黑盒”方式保护协议,它通过确保最终运行结果与协议预期设计相一致来增强安全性,而无需直接干涉合约代码执行。
不幸的是,EVM设计不支持在链上实现运行时保护,因为智能合约无法访问完整的运行时上下文。
如何克服这一挑战?我们认为以下先决条件是必要的:
一个专门的模块,可以访问跨智能合约的所有信息,包括整个交易上下文。
从智能合约获得必要的授权,使模块有权根据需要回撤(revert)交易。
确保模块的功能在智能合约执行后和状态提交之前生效。
我们推出了Aspect Programming,这是支持Artela区块链的一种编程框架,支持在区块链上进行原生扩展。
Aspect是可编程的原生扩展模块,用于在运行时动态集成自定义功能到区块链中,作为智能合约的模块化补充,增强链上功能性。

Aspect的特性是能够访问区块链基础层的系统级API,并在交易生命周期的各个切点(Join Point)添加扩展逻辑。智能合约可以绑定指定的Aspect以触发扩展功能。当交易调用智能合约时,该交易也会经由与该合约关联的Aspect处理。
Aspect可以记录每个函数调用的执行状态,并防止在回调函数执行期间发生重入。当在回调函数执行期间发生重入调用时,Aspect会检测到并立即回撤该交易,防止攻击者利用重入漏洞。通过这种方法,Aspect有效地消除了重入攻击,确保智能合约的安全性和稳定性。
Aspect实现运行时保护的关键属性:
可在智能合约执行后和状态提交前触发:Aspect模块可设置为在智能合约执行后但在状态提交前激活。
完整的交易上下文访问:Aspect可以访问完整的交易上下文,包括整个交易信息(方法,参数等)、调用栈(执行过程中所有内部合约调用)、状态上下文变更以及所有交易触发的事件。
系统调用能力:Aspect可以进行系统调用,并在必要时发起交易回撤。
与智能合约的绑定和授权:智能合约可以绑定到Aspect,并授予Aspect参与交易处理的权限。

本章我们探讨如何在链上实现Aspect的运行时保护。👇👇
可以在“preContractCall”和“postContractCall”的切点(Join Point)中部署一个实际的“合约保护意图”Aspect,以防止重入攻击。
💡💡
preContractCall: 在跨合约调用执行之前触发
postContractCall: 在跨合约调用执行后触发
为进行重入保护,我们的目标是在调用结束之前阻止合约重入。通过Aspect,我们可以通过在交易生命周期的切点处添加特定逻辑来实现这一目标。
在“preContractCall”切点中,Aspect监控合约调用堆栈。如果在调用堆栈中有任何重复调用(这意味着我们锁定的调用中出现了意外重入),Aspect将会回撤该调用。
/**
* preContractCall is a join-point which will be invoked before the contract call is executed.
*
* @param ctx context of the given join-point
* @return result of Aspect execution
*/
preContractCall(ctx: PreContractCallCtx): AspectOutput {
// Get the method of currently called contract.
let currentCallMethod = utils.praseCallMethod(ctx.currInnerTx!.data);
// Define functions that are not susceptible to reentrancy.
// - 0xec45ef89: sig of add_liquidity
// - 0xe446bfca: sig of remove_liquidity
let lockMethods = ["0xec45ef89", "0xe446bfca"];
// Verify if the current method is within the scope of functions that are not susceptible to reentrancy.
if (lockMethods.includes(currentCallMethod)) {
// Retrieve the call stack from the context, which refers to
// all contract calls along the path of the current contract method invocation.
let rawCallStack = ctx.getCallStack();
// Create a linked list to encapsulate the raw data of the call stack.
let callStack = utils.wrapCallStack(rawCallStack);
// Check if there already exists a non-reentrant method on the current call path.
callStack = callStack!.parent;
while (callStack != null) {
let callStackMethod = utils.praseCallMethod(callStack.data);
if (lockMethods.includes(callStackMethod)) {
// If yes, revert the transaction.
ctx.revert("illegal transaction: reentrancy attack");
}
callStack = callStack.parent;
}
}
return new AspectOutput(true);
}
我们编写了Curve模拟合约并复刻重入攻击,以更易理解的方式重现了这个过程。合约代码如下 👇
event AddLiquidity:
excuted: uint256
event RemoveLiquidity:
excuted: uint256
deployer: address
@external
def __init__():
self.deployer = msg.sender
@external
@view
def isOwner(user: address) -> bool:
return user == self.deployer
@external
@nonreentrant('lock')
def add_liquidity():
log AddLiquidity(1)
@external
@nonreentrant('lock')
def remove_liquidity():
raw_call(msg.sender, b"")
log RemoveLiquidity(1)
可以看到,上述合约的add_liquidity和remove_liquidity都由同一个重入锁lock进行了保护,这意味着如果重入保护正常工作,无法通过改锁重入被保护函数(例如,在remove_liquidity中调用add_liquidity)。
使用vyper编译器0.2.15、0.2.16或0.3.0(这些版本存在已知的重入保护问题)编译上述合约。
然后,我们部署上述受害者合约,并使用以下合约对其进行攻击 👇
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
interface CurveContract {
event AddLiquidity(uint256 executed);
event RemoveLiquidity(uint256 executed);
function add_liquidity() external;
function remove_liquidity() external;
}
contract Attack {
CurveContract public curve;
constructor(address _curveContract) {
curve = CurveContract(_curveContract);
}
function attack() external payable {
curve.remove_liquidity();
}
fallback() external {
curve.add_liquidity();
}
}
模拟实际攻击,此合约的attack方法尝试通过其fallback函数从remove_liquidity方法重入add_liquidity。如果实际发生了重入,可在receipt中观察到在RemoveLiquidity事件之前记录了一个AddLiquidity事件。
transaction receipt -> {
"txHash": ...,
"events": [{
"topic": "AddLiquidity",
...
}, {
"topic": "RemoveLiquidity",
...
}]
}

现在让我们使用Aspect来保护受攻击的合约。在执行以下操作之前,请先完成以下步骤:
部署Aspect
将受害合约与Aspect绑定
如果对Aspect操作不熟悉,可以首先查看我们的开发者指南先行了解。
完成上述操作后,现在让我们尝试再次调用attack方法,以检查操作是否会成功进行。

从动图中我们可以看到,重入交易已经被revert,这意味着我们的Aspect正在成功保护受害合约免受重入攻击。
最近对Curve的攻击再次说明了没有100%完全安全的协议。仅仅将重心放在协议的源代码和编译级别的安全性上是不足够的。即使源代码看起来毫无瑕疵,由于编译器问题,漏洞仍然可能意外出现。
为了增强DeFi的安全性,运行时保护变得至关重要。通过以“黑盒”方式保护协议,确保协议的执行与其预期设计一致,可以有效地防止运行时的重入攻击。
我们复刻了Curve合约并完全模拟了其近期的重入攻击,并以更易理解的方式再现了整个过程。利用Aspect编程作为一种新方法,实现链上运行时保护,我们逐步展示了如何用Aspect保护受害合约。我们的目标是帮助彻底消除Curve等DeFi协议可能遭受的递归攻击,从而增强整个DeFi领域的安全性。
作者:CP, Yuanyuan, Mike
联系方式:mike@artela.network 欢迎交流,批评指正
关注Artela的Twitter并加入我们的中文开发者社区,以便及时了解Artela的最新动态。也可以在我们的网站上了解更多关于Artela的信息
原文链接:
重入攻击仍然是一个挑战。现有的风险控制措施主要集中在协议源代码层面,并且仅在合约进入runtime状态前生效。
运行时保护是DeFi安全的重要补充,确保协议的执行与其预期设计一致。
EVM设计不支持运行时保护,因为智能合约无法访问runtime状态全部上下文信息。
Aspect Programming可以实现运行时保护,消除重入攻击。
我们逐步展示了如何通过Aspect防范对Curve合约的重入攻击。
尽管重入攻击是一个众所周知的问题,并且出现了许多风险控制措施,但在过去的两年中,涉及此类攻击的安全事件仍在不断发生:
Curve Finance攻击(2023年7月) - 6000万美元,Curve因其合约编程语言Vyper编译缺陷遭受重入攻击。
Origin Protocol攻击(2022年11月) - 700万美元,稳定币项目Origin Dollar(OUSD)遭受了重入攻击。
Siren Protocol攻击(2021年9月) - 350万美元,AMM池遭受重入攻击。
Cream Finance攻击(2021年8月) - 1880万美元,攻击者利用重入漏洞进行二次借贷。
目前,防范重入攻击的重点集中在智能合约的源代码层面,措施包括集成OpenZeppelin的ReentrancyGuard,以及对合约逻辑代码进行安全审计,以避免预定义的安全隐患。
这种方法被称为“白盒”解决方案,旨在在源代码层面规避漏洞,以最小化逻辑错误。然而,其主要挑战在于无法防御未知隐患。
将合约从源代码“转化”为实际runtime是个具有挑战性的过程。每一步可能为开发人员带来无法预料的问题,而合约源代码本身可能无法全面涵盖所有潜在情况。在Curve的案例中,由于编译器问题,即使协议源代码是正确的,最终运行结果与协议的预期设计之间仍可能存在差异。

仅仅依靠协议在源代码和编译层面的安全性是不足够的。即使源代码看起来毫无瑕疵,由于编译器问题,漏洞仍可能意外出现。
与现有的风险控制措施集中在协议源代码层面并在运行之前生效不同,运行时保护涉及协议开发人员编写运行时保护规则和操作,以处理运行时的未预料情况。 这有助于对运行时执行结果进行实时评估及应对。

运行时保护在增强DeFi安全性方面至关重要,是现有安全措施的重要补充。通过以“黑盒”方式保护协议,它通过确保最终运行结果与协议预期设计相一致来增强安全性,而无需直接干涉合约代码执行。
不幸的是,EVM设计不支持在链上实现运行时保护,因为智能合约无法访问完整的运行时上下文。
如何克服这一挑战?我们认为以下先决条件是必要的:
一个专门的模块,可以访问跨智能合约的所有信息,包括整个交易上下文。
从智能合约获得必要的授权,使模块有权根据需要回撤(revert)交易。
确保模块的功能在智能合约执行后和状态提交之前生效。
我们推出了Aspect Programming,这是支持Artela区块链的一种编程框架,支持在区块链上进行原生扩展。
Aspect是可编程的原生扩展模块,用于在运行时动态集成自定义功能到区块链中,作为智能合约的模块化补充,增强链上功能性。

Aspect的特性是能够访问区块链基础层的系统级API,并在交易生命周期的各个切点(Join Point)添加扩展逻辑。智能合约可以绑定指定的Aspect以触发扩展功能。当交易调用智能合约时,该交易也会经由与该合约关联的Aspect处理。
Aspect可以记录每个函数调用的执行状态,并防止在回调函数执行期间发生重入。当在回调函数执行期间发生重入调用时,Aspect会检测到并立即回撤该交易,防止攻击者利用重入漏洞。通过这种方法,Aspect有效地消除了重入攻击,确保智能合约的安全性和稳定性。
Aspect实现运行时保护的关键属性:
可在智能合约执行后和状态提交前触发:Aspect模块可设置为在智能合约执行后但在状态提交前激活。
完整的交易上下文访问:Aspect可以访问完整的交易上下文,包括整个交易信息(方法,参数等)、调用栈(执行过程中所有内部合约调用)、状态上下文变更以及所有交易触发的事件。
系统调用能力:Aspect可以进行系统调用,并在必要时发起交易回撤。
与智能合约的绑定和授权:智能合约可以绑定到Aspect,并授予Aspect参与交易处理的权限。

本章我们探讨如何在链上实现Aspect的运行时保护。👇👇
可以在“preContractCall”和“postContractCall”的切点(Join Point)中部署一个实际的“合约保护意图”Aspect,以防止重入攻击。
💡💡
preContractCall: 在跨合约调用执行之前触发
postContractCall: 在跨合约调用执行后触发
为进行重入保护,我们的目标是在调用结束之前阻止合约重入。通过Aspect,我们可以通过在交易生命周期的切点处添加特定逻辑来实现这一目标。
在“preContractCall”切点中,Aspect监控合约调用堆栈。如果在调用堆栈中有任何重复调用(这意味着我们锁定的调用中出现了意外重入),Aspect将会回撤该调用。
/**
* preContractCall is a join-point which will be invoked before the contract call is executed.
*
* @param ctx context of the given join-point
* @return result of Aspect execution
*/
preContractCall(ctx: PreContractCallCtx): AspectOutput {
// Get the method of currently called contract.
let currentCallMethod = utils.praseCallMethod(ctx.currInnerTx!.data);
// Define functions that are not susceptible to reentrancy.
// - 0xec45ef89: sig of add_liquidity
// - 0xe446bfca: sig of remove_liquidity
let lockMethods = ["0xec45ef89", "0xe446bfca"];
// Verify if the current method is within the scope of functions that are not susceptible to reentrancy.
if (lockMethods.includes(currentCallMethod)) {
// Retrieve the call stack from the context, which refers to
// all contract calls along the path of the current contract method invocation.
let rawCallStack = ctx.getCallStack();
// Create a linked list to encapsulate the raw data of the call stack.
let callStack = utils.wrapCallStack(rawCallStack);
// Check if there already exists a non-reentrant method on the current call path.
callStack = callStack!.parent;
while (callStack != null) {
let callStackMethod = utils.praseCallMethod(callStack.data);
if (lockMethods.includes(callStackMethod)) {
// If yes, revert the transaction.
ctx.revert("illegal transaction: reentrancy attack");
}
callStack = callStack.parent;
}
}
return new AspectOutput(true);
}
我们编写了Curve模拟合约并复刻重入攻击,以更易理解的方式重现了这个过程。合约代码如下 👇
event AddLiquidity:
excuted: uint256
event RemoveLiquidity:
excuted: uint256
deployer: address
@external
def __init__():
self.deployer = msg.sender
@external
@view
def isOwner(user: address) -> bool:
return user == self.deployer
@external
@nonreentrant('lock')
def add_liquidity():
log AddLiquidity(1)
@external
@nonreentrant('lock')
def remove_liquidity():
raw_call(msg.sender, b"")
log RemoveLiquidity(1)
可以看到,上述合约的add_liquidity和remove_liquidity都由同一个重入锁lock进行了保护,这意味着如果重入保护正常工作,无法通过改锁重入被保护函数(例如,在remove_liquidity中调用add_liquidity)。
使用vyper编译器0.2.15、0.2.16或0.3.0(这些版本存在已知的重入保护问题)编译上述合约。
然后,我们部署上述受害者合约,并使用以下合约对其进行攻击 👇
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
interface CurveContract {
event AddLiquidity(uint256 executed);
event RemoveLiquidity(uint256 executed);
function add_liquidity() external;
function remove_liquidity() external;
}
contract Attack {
CurveContract public curve;
constructor(address _curveContract) {
curve = CurveContract(_curveContract);
}
function attack() external payable {
curve.remove_liquidity();
}
fallback() external {
curve.add_liquidity();
}
}
模拟实际攻击,此合约的attack方法尝试通过其fallback函数从remove_liquidity方法重入add_liquidity。如果实际发生了重入,可在receipt中观察到在RemoveLiquidity事件之前记录了一个AddLiquidity事件。
transaction receipt -> {
"txHash": ...,
"events": [{
"topic": "AddLiquidity",
...
}, {
"topic": "RemoveLiquidity",
...
}]
}

现在让我们使用Aspect来保护受攻击的合约。在执行以下操作之前,请先完成以下步骤:
部署Aspect
将受害合约与Aspect绑定
如果对Aspect操作不熟悉,可以首先查看我们的开发者指南先行了解。
完成上述操作后,现在让我们尝试再次调用attack方法,以检查操作是否会成功进行。

从动图中我们可以看到,重入交易已经被revert,这意味着我们的Aspect正在成功保护受害合约免受重入攻击。
最近对Curve的攻击再次说明了没有100%完全安全的协议。仅仅将重心放在协议的源代码和编译级别的安全性上是不足够的。即使源代码看起来毫无瑕疵,由于编译器问题,漏洞仍然可能意外出现。
为了增强DeFi的安全性,运行时保护变得至关重要。通过以“黑盒”方式保护协议,确保协议的执行与其预期设计一致,可以有效地防止运行时的重入攻击。
我们复刻了Curve合约并完全模拟了其近期的重入攻击,并以更易理解的方式再现了整个过程。利用Aspect编程作为一种新方法,实现链上运行时保护,我们逐步展示了如何用Aspect保护受害合约。我们的目标是帮助彻底消除Curve等DeFi协议可能遭受的递归攻击,从而增强整个DeFi领域的安全性。
作者:CP, Yuanyuan, Mike
联系方式:mike@artela.network 欢迎交流,批评指正
关注Artela的Twitter并加入我们的中文开发者社区,以便及时了解Artela的最新动态。也可以在我们的网站上了解更多关于Artela的信息
No activity yet