解码以太坊交易以防止UI欺骗

  • cyfrin
  • 发布于 4天前
  • 阅读 34

本文介绍了如何手动解码以太坊calldata以检测UI欺骗攻击。通过解析交易数据,用户可以验证交易的意图,防止恶意操作。文章详细讲解了ERC-20授权交易的解码过程,并提供了一个Python脚本来自动化calldata分析,最终能够帮助开发者在签名恶意DApp交易之前检测和预防UI欺骗攻击。

Valentina Rivas

保护 dApp 免受 UI 欺骗攻击(第一部分):解码交易

学习如何使用 Python 解码 Ethereum calldata,以在签署恶意 dApp 交易之前检测并预防 UI 欺骗攻击。

在本教程中,我们将分解如何手动解码 Ethereum calldata,以检测 UI 欺骗攻击。这种基础技能使你能够在签名之前验证交易的意图。

UI 欺骗攻击会操纵交易界面,欺骗用户批准恶意操作。在这个分为两部分的系列中,我们为开发人员配备了工具,通过验证交易数据来检测这些攻击。第一部分重点介绍解码原始 Ethereum calldata——每个交易核心的十六进制指令。你将学习:

  • 手动解码 ERC-20 批准交易
  • 提取关键参数,如接收者地址和金额
  • 使用 Python 脚本自动执行 ERC-20 calldata 分析

在本指南结束时,你将能够解析字节级别的交易数据,这是一项检测恶意活动的重要技能。

什么是 UI 欺骗攻击?

UI 欺骗攻击是一类网络攻击,其中攻击者操纵用户界面,诱骗受害者执行非预期的操作。

区块链中的 UI 欺骗

当应用于 web3 时,UI 欺骗攻击涉及将有害区块链交易伪装成合法操作的恶意界面。攻击者会更改显示的详细信息,如接收者地址、代币数量和合约函数。例如:

  • 一个 dApp 可能会显示“批准 100 USDC 给 Uniswap”,但编码的交易是将资金批准给攻击者的地址。

  • 恶意软件可能会更改钱包界面中显示的数据,以隐藏真实的接收者或金额。

考虑一下这种情况:Alex 打开了他的钱包应用程序,准备交换代币。界面显示了一个熟悉的批准请求:“批准 100 USDC 给(地址…)。” 看起来很正常,所以 Alex 点击了确认。片刻之后,他的资金消失了。发生了什么事?

Alex 刚刚成为了 UI 欺骗攻击的受害者,攻击者操纵了钱包的界面,以隐藏资金的真实接收者。Alex 签署的交易并没有批准发送 USDC 来执行预期的操作。它批准了对恶意合约的无限制提款。

这些攻击利用了用户_看到_的和区块链_执行_的之间的差距。

近期 UI 欺骗攻击

在去中心化金融(DeFi)的兴起和复杂的社会工程策略的推动下,UI 欺骗攻击在 web3 中变得越来越普遍。备受瞩目的事件包括:

  1. Bybit 的 14 亿美元盗窃案(2025 年):攻击者入侵了 Safe Wallet 的基础设施,将恶意 JavaScript 注入到 UI 中,诱骗 Bybit 的多重签名者批准耗尽资金的交易。这仍然是加密货币历史上最大的 UI 欺骗攻击(与社会工程相结合)。

  2. Radiant Capital 5000 万美元黑客攻击(2025 年):攻击者使用伪装成合法 PDF 文件的恶意软件入侵了三名开发人员的设备。该恶意软件将恶意代码注入到 Safe Wallet 的界面中,显示良性的交易详细信息,同时秘密地将恶意的 calldata 发送到硬件钱包。这使得攻击者可以绕过多重签名验证并从借贷池中耗尽 5000 万美元以上的资金。

  3. Ledger Connect Kit 漏洞利用(2023 年):一次供应链攻击将代码注入到 Ledger 的库中,更改了钱包 UI,提示用户进行无限制的批准,在缓解之前吸走了 60 万美元以上的资金。

为什么 calldata 验证很重要

每个 Ethereum 交易都包含 calldata——发送到智能合约的编码指令。通过验证 calldata,用户和开发人员可以:

  1. 确认接收者、金额和被调用的函数。
  2. 在签署或执行交易之前对其进行验证。

本教程的范围

本指南假定你具备:

本教程专门侧重于 ERC-20 代币的批准,这是去中心化金融(DeFi)中最常见和安全关键的交易之一。通过修改 ABI 定义和解码逻辑,所演示的技术可以适用于其他类型的交易(转账、交换等)。

提供的脚本是教育示例,用于演示 calldata 验证的概念,并在上面的 Radiant Capital 链接中进行了说明。对于生产用途,请扩展这些工具以处理更广泛的功能和边缘情况。

calldata 的结构

Calldata 是一个包含两个元素的不可变十六进制字符串:

  • 函数选择器Calldata 的前四个字节编码函数选择器,该选择器唯一标识被调用的函数(例如,0x095ea7b3 对应于 ERC-20 approve 函数)。“0x” 前缀表示一个十六进制数,其余八个字符等于四个字节。

提示:为了确保函数选择器对应于预期的函数,你可以将其与已知函数签名数据库(例如 4byte.directory)中的条目进行比较。

  • 编码参数:在函数选择器之后,每个参数根据 ABI 规范编码为 32 字节的段。即使实际数据较小(例如地址或较小的整数),也会用零填充以满足 32 字节的要求,遵循 ABI 编码规则

让我们探索一笔应该将 25 USDC 发送到特定地址的交易。当你在钱包的 UI 中看到这样的十六进制数据时,目标是验证它是否真的会按照它声称的那样去做。

MetaMask 交易确认屏幕显示了 25 个 USDC 的批准请求,并高亮显示了一个图标以显示原始 calldata 以进行验证。

示例:解码 ERC-20 批准 calldata

让我们检查一下如何手动解码 ERC-20 批准 calldata。考虑以下十六进制 calldata

0x095ea7b30000000000000000000000006a000f20005980200259b80c510200304000106800000000000000000000000000000000000000000000000000000000017d7840

分解如下:

  1. 函数选择器0x095ea7b3 标识函数 approve(address,uint256)

  2. 参数 1:Spender 地址(接下来的 64 个十六进制字符,已填充):

0000000000000000000000006a000f20005980200259b80c510200304000106800
  • 实际地址是最后 40 个字符:6a000f20005980200259b80c5102003040001068
  • 以校验和格式:0x6A000F20005980200259B80c5102003040001068
  1. 参数 2金额(最后的 64 个十六进制字符,已填充):
00000000000000000000000000000000000000000000000000000000017d7840
  • 0x17d7840 = 25,000,000 个原始单位
  • 使用 USDC 的六位小数:25.0 USDC

现在我们已经手动解码了 calldata,我们可以看到这个过程是如何在底层工作的。但是,手动解析十六进制字符串很容易出错,并且对于常规使用来说可能不切实际。让我们用 Python 实现这个解码逻辑来自动化该过程并使其更可靠。

准备工作

在下一节中实现 Python 代码之前,请通过在你的终端或你选择的 IDE 上运行以下命令来安装必要的库:

pip install web3 eth-abi eth-utils

使用 Python 进行 calldata 的基本解码

下面是一个 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 部分中,我们将:

  • 使用本地 Ethereum 分叉模拟交易
  • 比较预期与实际状态变化
  • 自动检测恶意 calldata 差异
  • 分析现实世界中的 UI 欺骗攻击向量
  • 原文链接: cyfrin.io/blog/secure-da...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
cyfrin
cyfrin
Securing the blockchain and its users. Industry-leading smart contract audits, tools, and education.