Trojan Tactics:合约恶意代码隐藏与蜜罐

Chiu Lv4

你看到的代码不一定是你执行的代码

今日聊聊怎么隐藏恶意代码以及怎么利用这种手段来制造蜜罐


一、利用外部合约隐藏恶意代码(Trojan)

Solidity 中,任何地址都可以被“强制类型转换”为任意合约类型。你以为在执行某个合约的函数,实际上却在执行另一个地址上的代码。

看似无辜的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
contract Foo {
Bar bar;

constructor(address _bar) {
bar = Bar(_bar);
}

function callBar() public {
bar.log();
}
}

contract Bar {
event Log(string message);

function log() public {
emit Log("Bar was called");
}
}

Foo.callBar() 看似安全,他调用Bar合约的log函数来记录日志
但是bar对象来源于外部传入的地址,而不是内部使用new方法来实例化

恶意代码:

攻击者部署Foo合约的时候,实际传给constructor的地址的并不是 Bar 的地址,而是另一个我们看不到的合约 —— Mal

1
2
3
4
5
6
7
8
contract Mal {
event Log(string message);

function log() public {
emit Log("Mal was called");
// 在这里执行恶意操作
}
}

所以当你调用 Foo.callBar(),执行的是Mal合约中伪造的log()函数


Tips:

  • 避免依赖外部传入的合约地址
  • construtor设置为public,以便审查传入的地址
  • 更安全的做法:在构造函数中直接创建依赖合约 bar = new Bar()

二、蜜罐(Honey Pot)

Background:

当攻击者审到这段代码

1
2
3
4
5
6
7
8
9
10
function withdraw(uint _amount) public {
require(_amount <= balances[msg.sender], "Insufficient funds");

(bool sent,) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send Ether");

balances[msg.sender] -= _amount;

logger.log(msg.sender, _amount, "Withdraw");
}

一眼重入,鉴定为状态更新写晚了,直接狠狠地进行一个调用的嵌套


Exploit:

攻击者部署如下合约尝试攻击:

1
2
3
4
5
6
7
8
9
10
   function attack() public payable {
bank.deposit{value: 1 ether}();
bank.withdraw(1 ether);
}

fallback() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw(1 ether);
}
}

存入 1 ether,调用 withdraw(),妄图重入攻击榨干合约


蜜罐

但实际上,Bank 合约调用的 logger.log() 实际上并不是 Logger,而是利用上述Trojan技巧隐藏的蜜罐:

1
2
3
4
5
6
7
8
9
10
11
contract HoneyPot {
function log(address _caller, uint256 _amount, string memory _action) public {
if (equal(_action, "Withdraw")) {
revert("It's a trap");
}
}

function equal(string memory _a, string memory _b) public pure returns (bool) {
return keccak256(abi.encode(_a)) == keccak256(abi.encode(_b));
}
}

只要是尝试Withdraw行为,蜜罐就会 revert(),让整个交易失败,操作回滚

知识补充

为什么不直接把_action参数与Withdraw字符串进行比较
因为_action是string类型,本质是bytes的动态数组,存放在memory中,而solidity的==操作符默认不允许用于动态内存数组的比较
否则编译时会报错:
TypeError: Operator == not compatible with types string memory and string literal.
所以这里使用keccak256(abi.encodePacked(...))讲动态长度的string压缩成定长的bytes32 hash


通过隐藏代码欺骗用户,通过蜜罐欺骗攻击者,唉唉,欺骗的艺术

  • Title: Trojan Tactics:合约恶意代码隐藏与蜜罐
  • Author: Chiu
  • Created at : 2025-04-23 21:55:53
  • Updated at : 2025-07-15 13:01:35
  • Link: https://github.com/Idealist17/github.io/2025/04/23/合约恶意代码隐藏与蜜罐/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments