anthor: S7iter

对于没开源的后面再写,主要是类似于训练数据集的形式,非使用glider,后续在讲。
glider也可以查询opcode,不过我还没测试过….

写在前面

glider有点类似于codeql。不过是属于链上版本的(CodeQL for Smart Contracts)。
doc:https://glide.gitbook.io/main/glider-ide
相对于etherscan原生的工具来说:
https://etherscan.io/searchcontract 根据合约代码查找
https://etherscan.io/find-similar-contracts 根据智能合约查找
有如下优点:
可编程 + 灵活的查询语言
处理所有部署的合约 (比如一些合约开源,可以通过其特征查找)
更加自动化+组合过滤:比如你研究了某个仓库的某个方法可能在某些情况下出现一些问题,那么完全可以只搜索这个单一方法的功能而非function名称,或者调用状态等等
同时:Glider 的 query 组件里有很多工具,比如对函数/事件/状态变量/修饰符等属性的支持,以及对继承、接口、struct 等语言结构的访问。这使得你可以组合复杂查询(比如 “找出所有合约中有某种 modifier +外部调用 +状态变量写入”的情况)。Etherscan 的搜索虽然有过滤器,但通常没有这么细粒度也不能作为程序化查询。

Use

没什么好写的,看文档就行
语法也比较简单,Python写的,相当于只用一个库,来写代码就行了。

实战

这里真实合约漏洞为例。

这里以spoke合约受攻击为例:

受害者合约代码
https://app.sentio.xyz/contract/56/0xdf4fFDa22270c12d0b5b3788F1669D709476111E?t=1
攻击合约
https://bscscan.com/address/0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1
攻击者地址:
https://bscscan.com/address/0xc6e8210e47602860c97edc0bd7556641f048868e

真实的一起攻击事件:
tx: 0xcd345310e491195f0500d45d6987eaef342bae24390f4da4f7e6749b8105b4c3

这个漏洞调用栈比较少, 跟着调用流程debug:

可以看到先进行sponsorOrderUsingPermit2()函数操作。

sponsorOrderUsingPermit2()接收3个参数()signature不说了。
看传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
order:[
{
fromAddress:"0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1"
toAddress:"0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1"
filler:"0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1"
fromToken:"0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d" //USDC
toToken:"0x0000000000000000000000000000000000000000"
expiry:"1,759,825,777"
fromAmount:"15,121,014,087,537,935,893,820"
fillAmount:"0"
feeRate:"0"
fromChain:"56"
toChain:"56"
postHookHash:"0x0000000000000000000000000000000000000000000000000000000000000000"
}
1
2
3
4
5
6
permit:[
{permitted:[
{token:"0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1"
amount:"15,121,014,087,537,935,893,820"}]
nonce:"59,946,715,367,315,085,452,573,067,176,193,223,667,943,792,355,376,429,741,185,393,233,268,031,513,012"
deadline:"1,759,825,777"}

根据参数可以拿出来主要信息:
order.fromToken:”0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d” //USDC
permit.permitted.token:”0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1” //攻击者合约,里面有erc20实现,所以被认为token。

看一下sponsorOrderUsingPermit2方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// @inheritdoc ISpoke
function sponsorOrderUsingPermit2(
Order calldata order,
IPermit2.PermitTransferFrom calldata permit,
bytes calldata signature
) external nonReentrant {
bytes32 orderHash = keccak256(abi.encode(order));

if (orderHashToStatus[orderHash] != OrderStatus.EMPTY) revert OrderAlreadyExists();
if (block.timestamp > order.expiry) revert OrderExpired();
if (order.fromChain != _getChainId()) revert InvalidSourceChain();
if (order.fromToken == Utils.NATIVE_TOKEN) revert NativeTokensNotAllowed();

orderHashToStatus[orderHash] = OrderStatus.CREATED;

IPermit2.SignatureTransferDetails memory transferDetails;
transferDetails.to = address(this);
transferDetails.requestedAmount = order.fromAmount;

bytes32 witness = _hashOrderTyped(order);
permit2.permitWitnessTransferFrom(
permit,
transferDetails,
order.fromAddress,
witness,
Utils.ORDER_WITNESS_TYPE_STRING,
signature
);

emit OrderCreated(orderHash, order);
}

可以看出来该函数是一个用于创建订单的接口。
通过 Permit2(这里不详细解释了) 机制,从 order.fromAddress 转移 permit.permitted.token 的 order.fromAmount 到合约,创建订单并标记状态为 CREATED。

还可以跨链或同链代币交易场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct Order {
// Address that will supply the fromAmount of fromToken on the fromChain.
address fromAddress;
// Address to receive the fillAmount of toToken on the toChain.
address toAddress;
// Address that will fill the Order on the toChain.
address filler;
// Address of the ERC20 token being supplied on the fromChain.
// 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE in case of native token.
address fromToken;
// Address of the ERC20 token being supplied on the toChain.
// 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE in case of native token.
address toToken;
// Expiration in UNIX for the Order to be created on the fromChain.
uint256 expiry;
// Amount of fromToken to be provided by the fromAddress.
uint256 fromAmount;
// Amount of toToken to be provided by the filler.
uint256 fillAmount;
// Protocol fees are taken out of the fromAmount and are calculated within the Spoke.sol
// contract for single chain orders or on the Hub for cross chain orders.
// The following formula determines the amount of fromToken reserved as fees:
// fee = (fromAmount * feeRate) / 1000000
uint256 feeRate;
// Chain ID of the chain the Order will be created on.
uint256 fromChain;
// Chain ID of the chain the Order will be filled on.
uint256 toChain;
// Keccak256 hash of the abi.encoded ISquidMulticall.Call[] calldata calls that should be provided
// at the time of filling the order.
bytes32 postHookHash;
}

根据攻击者输入结合合约代码可以看出:合约未验证 permit.permitted.token == order.fromToken
那么攻击流程就很明了了:
攻击者存入假代币
记录USDC
后续可以通过 refundOrder() 提取USDC

现在类比下整个流程:

  • 你告诉银行(合约)要存 1000 USDC(order.fromToken)
  • 但你通过签名(permit)实际给了 1000 张废纸(假代币)
  • 银行记录你存了 1000 USDC(orderHashToStatus = CREATED)
  • 后来你说“退款refundOrder ”,银行按记录退给你 1000 USDC,而没检查你实际存了什么钱

现在我们就去编写出来这一查询

查询

先定位合约的主要特征:
这里我们先做个严格的查询,后续在写宽松查询:

1.确定目标函数

比如合约名字为:sponsorOrderUsingPermit2
funcs = Functions().with_name("sponsorOrderUsingPermit2").exec(20)
这样我们就可以只聚焦在实现中使用该函数名的部分,而不需要全网扫描所有函数。

2.理解漏洞机制

这个合约的功能实现的一个重点是使用了Permit2。
permitWitnessTransferFrom是 Uniswap 的 Permit2 合约的一个关键函数
比如允许用户通过签名授权代币从其地址转移到指定目标地址

1
2
3
4
5
6
7
8
function permitWitnessTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes32 witness,
string calldata witnessTypeString,
bytes calldata signature
) external;

也就是说,它允许在无需批准 (approve) 的情况下,直接基于签名进行代币转移。
但是,如果合约没有正确验证 permit.permitted.tokenorder.fromToken 一致,攻击者就可以伪造一个签名,
把假代币地址传进去,导致“用假币换真币”的严重漏洞。

3.编写

限制查询

基于这个思路,我们编写一个查询脚本,去检测哪些合约在使用 permitWitnessTransferFrom 时没有进行 permit.permitted.token == order.fromToken 的验证。

这里的token_checks_patterns写的有很大问题,因为实际情况可能完全不是这样,仅表示一个例子。
因为需要完全严格检查permitted.token == order.fromToken的验证,并不只是require

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from glider import *

def query():
results = []

funcs = Functions().with_name("sponsorOrderUsingPermit2").exec(50)

for func in funcs:
try:
src = func.source_code() or ""
except Exception:
continue

if "permitWitnessTransferFrom" not in src:
continue

if not any(pat in src for pat in [
'permit.permitted.token == order.fromToken',
'permit.permitted.token != order.fromToken',
'require(permit.permitted.token == order.fromToken',
'require(permit.permitted.token != order.fromToken',
'assert(permit.permitted.token == order.fromToken',
'assert(permit.permitted.token != order.fromToken'
]):
try:
addr = func.get_contract().address() if func.get_contract() else "unknown"
print(f"[VULN] vulnerable function found {addr}")
except Exception:
print("[VULN] vulnerable function found (address unavailable)")

results.append(func)

return results

运行结果

宽松查询

我们对于这个简单的查询来说可以:
不指定函数名:直接在全合约范围内查找 permitWitnessTransferFrom
用正则匹配函数名:匹配包含 OrderPermitSwap 等常见关键词的函数名,例如 with_name_regex("^.*Order.*$")。(测试了有bug)
但仍保留对 permit.permitted.token == order.fromToken 这类“必须存在的校验”作为核心检测条件

具体实现就不写了,因为还有一些合约存在这种变式漏洞。