本文介绍了如何手动解码以太坊calldata以检测UI欺骗攻击。通过解析交易数据,用户可以验证交易的意图,防止恶意操作。文章详细讲解了ERC-20授权交易的解码过程,并提供了一个Python脚本来自动化calldata分析,最终能够帮助开发者在签名恶意DApp交易之前检测和预防UI欺骗攻击。
Valentina Rivas
学习如何使用 Python 解码 Ethereum calldata,以在签署恶意 dApp 交易之前检测并预防 UI 欺骗攻击。
在本教程中,我们将分解如何手动解码 Ethereum calldata,以检测 UI 欺骗攻击。这种基础技能使你能够在签名之前验证交易的意图。
UI 欺骗攻击会操纵交易界面,欺骗用户批准恶意操作。在这个分为两部分的系列中,我们为开发人员配备了工具,通过验证交易数据来检测这些攻击。第一部分重点介绍解码原始 Ethereum calldata——每个交易核心的十六进制指令。你将学习:
在本指南结束时,你将能够解析字节级别的交易数据,这是一项检测恶意活动的重要技能。
UI 欺骗攻击是一类网络攻击,其中攻击者操纵用户界面,诱骗受害者执行非预期的操作。
当应用于 web3 时,UI 欺骗攻击涉及将有害区块链交易伪装成合法操作的恶意界面。攻击者会更改显示的详细信息,如接收者地址、代币数量和合约函数。例如:
一个 dApp 可能会显示“批准 100 USDC 给 Uniswap”,但编码的交易是将资金批准给攻击者的地址。
恶意软件可能会更改钱包界面中显示的数据,以隐藏真实的接收者或金额。
考虑一下这种情况:Alex 打开了他的钱包应用程序,准备交换代币。界面显示了一个熟悉的批准请求:“批准 100 USDC 给(地址…)。” 看起来很正常,所以 Alex 点击了确认。片刻之后,他的资金消失了。发生了什么事?
Alex 刚刚成为了 UI 欺骗攻击的受害者,攻击者操纵了钱包的界面,以隐藏资金的真实接收者。Alex 签署的交易并没有批准发送 USDC 来执行预期的操作。它批准了对恶意合约的无限制提款。
这些攻击利用了用户_看到_的和区块链_执行_的之间的差距。
在去中心化金融(DeFi)的兴起和复杂的社会工程策略的推动下,UI 欺骗攻击在 web3 中变得越来越普遍。备受瞩目的事件包括:
Bybit 的 14 亿美元盗窃案(2025 年):攻击者入侵了 Safe Wallet 的基础设施,将恶意 JavaScript 注入到 UI 中,诱骗 Bybit 的多重签名者批准耗尽资金的交易。这仍然是加密货币历史上最大的 UI 欺骗攻击(与社会工程相结合)。
Radiant Capital 5000 万美元黑客攻击(2025 年):攻击者使用伪装成合法 PDF 文件的恶意软件入侵了三名开发人员的设备。该恶意软件将恶意代码注入到 Safe Wallet 的界面中,显示良性的交易详细信息,同时秘密地将恶意的 calldata 发送到硬件钱包。这使得攻击者可以绕过多重签名验证并从借贷池中耗尽 5000 万美元以上的资金。
Ledger Connect Kit 漏洞利用(2023 年):一次供应链攻击将代码注入到 Ledger 的库中,更改了钱包 UI,提示用户进行无限制的批准,在缓解之前吸走了 60 万美元以上的资金。
每个 Ethereum 交易都包含 calldata——发送到智能合约的编码指令。通过验证 calldata,用户和开发人员可以:
本指南假定你具备:
本教程专门侧重于 ERC-20 代币的批准,这是去中心化金融(DeFi)中最常见和安全关键的交易之一。通过修改 ABI 定义和解码逻辑,所演示的技术可以适用于其他类型的交易(转账、交换等)。
提供的脚本是教育示例,用于演示 calldata 验证的概念,并在上面的 Radiant Capital 链接中进行了说明。对于生产用途,请扩展这些工具以处理更广泛的功能和边缘情况。
Calldata 是一个包含两个元素的不可变十六进制字符串:
0x095ea7b3
对应于 ERC-20 approve
函数)。“0x” 前缀表示一个十六进制数,其余八个字符等于四个字节。提示:为了确保函数选择器对应于预期的函数,你可以将其与已知函数签名数据库(例如 4byte.directory)中的条目进行比较。
让我们探索一笔应该将 25 USDC 发送到特定地址的交易。当你在钱包的 UI 中看到这样的十六进制数据时,目标是验证它是否真的会按照它声称的那样去做。
让我们检查一下如何手动解码 ERC-20 批准 calldata。考虑以下十六进制 calldata:
0x095ea7b30000000000000000000000006a000f20005980200259b80c510200304000106800000000000000000000000000000000000000000000000000000000017d7840
分解如下:
函数选择器:0x095ea7b3
标识函数 approve(address,uint256)
参数 1:Spender 地址(接下来的 64 个十六进制字符,已填充):
0000000000000000000000006a000f20005980200259b80c510200304000106800
6a000f20005980200259b80c5102003040001068
0x6A000F20005980200259B80c5102003040001068
00000000000000000000000000000000000000000000000000000000017d7840
0x17d7840
= 25,000,000 个原始单位现在我们已经手动解码了 calldata,我们可以看到这个过程是如何在底层工作的。但是,手动解析十六进制字符串很容易出错,并且对于常规使用来说可能不切实际。让我们用 Python 实现这个解码逻辑来自动化该过程并使其更可靠。
在下一节中实现 Python 代码之前,请通过在你的终端或你选择的 IDE 上运行以下命令来安装必要的库:
pip install web3 eth-abi eth-utils
下面是一个 Python 函数,用于解码 ERC-20 批准 calldata,将其转换为人类可读的信息。
from eth_abi import decode
from eth_utils import to_checksum_address
def decode_erc20_approve(calldata: str, token_decimals: int = 18) -> dict:
"""
使用人类可读的输出解码 ERC-20 批准 calldata
参数:
- calldata: 十六进制 calldata 字符串
- token_decimals: 代币的小数位数(默认值:18)
返回:包含解码信息的字典
"""
# 如果存在,删除“0x”前缀
hex_data = calldata[2:] if calldata.startswith("0x") else calldata
# 提取函数选择器和参数
selector = hex_data[:8]
params_hex = hex_data[8:]
# 使用 eth_abi.decode 解码参数(相当于早期版本中的 decode_abi)
# 注意:我们显式地使用 decode 作为 ABI 解码器函数
spender, amount = decode(["address", "uint256"], bytes.fromhex(params_hex))
# 转换为人类可读的格式
spender_address = to_checksum_address(spender)
amount_raw = amount
amount_normalized = amount / (10 ** token_decimals)
return {
"function": "approve(address,uint256)",
"selector": f"0x{selector}",
"spender": spender_address,
"amount_raw": amount_raw,
"amount_normalized": amount_normalized,
"token_decimals": token_decimals
}
## 示例用法
calldata = "0x095ea7b30000000000000000000000006a000f20005980200259b80c510200304000106800000000000000000000000000000000000000000000000000000000017d7840"
result = decode_erc20_approve(calldata, token_decimals=6) # USDC 有 6 位小数
print("Calldata Breakdown:")
print(f"Function: {result['function']}")
print(f"Function Selector: {result['selector']}")
print(f"Spender Address: {result['spender']}")
print(f"Raw Amount: {result['amount_raw']}")
print(f"Normalized Amount: {result['amount_normalized']} USDC")
print(f"\nSummary: Approving {result['amount_normalized']} USDC to {result['spender']}")
完整的代码也可以作为 GitHub Gist 获得。
输出:
Calldata Breakdown:
Function: approve(address,uint256)
Function Selector: 0x095ea7b3
Spender Address: 0x6A000F20005980200259B80c5102003040001068
Raw Amount: 25000000
Normalized Amount: 25.0 USDC
Summary: Approving 25.0 USDC to 0x6A000F20005980200259B80c5102003040001068
注意:_我们显式地使用 eth_abi.decode
作为 ABI 解码器函数,这是生态系统当前的标准。_
此解码器从 ERC-20 批准交易中提取必要参数,将它们转换为人类可读的形式。它使用户能够手动验证交易是否与 UI 声称的内容匹配。
解码 calldata 告诉我们交易应该做什么,但模拟交易会告诉我们链上发生了什么。
你现在已经掌握了解码 ERC-20 批准交易的方法,即通过分析函数选择器和 ABI 编码的参数。虽然这揭示了交易应该做什么,但我们仍然需要以编程方式验证它实际在链上做什么。在第 2 部分中,我们将:
- 原文链接: cyfrin.io/blog/secure-da...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!