从一起“盗币”事件再谈合约安全问题

摘要:
=0); 要求(block.number˃map[uint256(消息发送者)+x]);require(block.blockhash(map[uint256(msg.sender)+x])!
从一起“盗币”事件再谈合约安全问题

本来是受到从一起“盗币”事件看以太坊存储 hash 碰撞问题一文启发,但是我并不太认同文中的观点.并且文中有一些技术性错误.

一. 起因

今日某安全厂商在以太坊上发布一份让大家来"盗币"的合约,就是希望大家能够意识到不好的合约设计会存在严重安全隐患.下面是这份合约源码.

pragma solidity ^0.4.21;
contract DVPgame {
    ERC20 public token;
    uint256[] map;
    using SafeERC20 for ERC20;
    using SafeMath for uint256;
    constructor(address addr) payable{
        token = ERC20(addr);
    }
    function (){
        if(map.length>=uint256(msg.sender)){
            require(map[uint256(msg.sender)]!=1);
        }
        if(token.balanceOf(this)==0){
            //airdrop is over
            selfdestruct(msg.sender);
        }else{
            token.safeTransfer(msg.sender,100);

            if (map.length <= uint256(msg.sender)) {
                map.length = uint256(msg.sender) + 1;
            }
            map[uint256(msg.sender)] = 1;  

        }
    }
    //Guess the value(param:x) of the keccak256 value modulo 10000 of the future block (param:blockNum)
    function guess(uint256 x,uint256 blockNum) public payable {
        require(msg.value == 0.001 ether || token.allowance(msg.sender,address(this))>=1*(10**18));
        require(blockNum>block.number);
        if(token.allowance(msg.sender,address(this))>0){
            token.safeTransferFrom(msg.sender,address(this),1*(10**18));
        }
        if (map.length <= uint256(msg.sender)+x) {
            map.length = uint256(msg.sender)+x + 1;
        }

        map[uint256(msg.sender)+x] = blockNum;
    }
    //Run a lottery
    function lottery(uint256 x) public {
        require(map[uint256(msg.sender)+x]!=0);
        require(block.number > map[uint256(msg.sender)+x]);
        require(block.blockhash(map[uint256(msg.sender)+x])!=0);
        uint256 answer = uint256(keccak256(block.blockhash(map[uint256(msg.sender)+x])))%10000;
        if (x == answer) {
            token.safeTransfer(msg.sender,token.balanceOf(address(this)));
            selfdestruct(msg.sender);
        }
    }
}

上述文中提到这里面安全问题是因为solidity在存储map时候的地址计算方式,存在hash碰撞问题,所以导致币被盗走. 但是显然并不是因为hash碰撞问题. 确实不好的设计会导致hash碰撞问题,但是这里确实不是hash碰撞引起的问题.

二. solidity复杂变量的地址计算问题

一个示例

开始之前,我们先找一个兼具各种元素

pragma solidity ^0.4.23; 
contract Locked {
    bool public unlocked = false;    
    struct NameRecord { 
        bytes32 name;
        address mappedAddress;
    }
    mapping(address => NameRecord) public registeredNameRecord; 
    mapping(bytes32 => address) public resolve;
    NameRecord []records;
    function register(bytes32 _name, address _mappedAddress) public {
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 
        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 
        require(unlocked); 
    }
    function newRecords(uint256 index,bytes32 _name, address _mappedAddress) public{
        NameRecord memory newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 
        if(recor)
        records[index]=newRecord;
        require(unlocked);
    }
}

简单变量的地址

每个合约都会有自己独立的存储空间(storage),运行时的Memory空间.storage和memory空间都是从0开始.
因为EVM是一个256位的虚拟机,因此Storage空间有2**256*256位这么大.
作为Locked这份合约中第一个简单变量unlcoked的地址就是0.
基本类型int,string,bytes32,固定大小的数组等都是简单类型,他们有固定的长度. 很容易算出来占用多少字节空间,因此只需依次累加即可.
比如registeredNameRecord的地址是1,resolve的地址是2,records地址就是3
另外就是要注意空间对齐问题

动态数组以及Map的地址

Array计算问题

因为动态数组,比如这里的records事先无法预知大小,他的地址计算就会用到hash. 简单来说,这里records中元素的起始地址就是hash(slot),这里的slot是3,因为records是第四个变量.
这个hash(slot)就是这个数组的起始地址,真正存储的变量地址则是hash(slot)+offset,offset的计算方式就和其他所有语言的offset计算方式都一样i*sizeof(NameRecord).

这种方式的好处就在于节省Gas,虽然定义了records对象,但是在你没存储任何对象之前,不会浪费一点Gas,要知道存储一个字就是20000Gas,成本昂贵.

而slot3,也就是3这个地址存的是数组的长度.

Map地址计算问题

Map的存储设计方式类似于Array,一样为了节省Gas,采用hash计算地址.和Array不一样的是,他是Hash(key,slot)而不是简单的slot. 以resolve这个map为例,"arandname"存储地址就是hash(bytes32("arandnme"),uint256(2).

如果存储对象比较复杂,不止占用一个字的存储空间,按照顺序递增即可.

三. 先来玩demo

newRecords函数成功调用,必须要求unlocked为true,但是unlocked并没有可以修改的地方. 这是一个棘手的问题,实际上这个是最前面合约问题的简化.

首先我们知道unlocked的存储地址为0
其次我们已经知道了动态数组的地址计算规则,那么是否可以让records[index]计算结果是0呢?

这个地址我们已经知道是hash(records_slot)+index*sizeof(NameRecord).
有了这个公式其实已经比较容易算出来了.

让我们来一步一步计算这个地址吧.

部署合约

这一步比较容易在Remix中选择Javascript VM方式直接部署即可.

部署Locked

初次调用newRecords

应该说绝大多数时候newRecords肯定是无法直接调用成功,那就先来一次失败调用吧.
我们就传入参数
0,"0x3131313131313131313131313131313131313131313131313131313131313131","0x692a70d2e424a56d2c6c27aa97d1a86395877b3a"
可以看到调用失败了,失败结果如下:
失败调用

从失败中找到正确方法

常言说,失败乃成功之母,我们就从失败中寻找成功吧.

Debug去找寻存储地址hash(records_slot)

单击Debug开始找寻地址的旅程吧.

这整个过程只有最后的records[index]=newRecord会在storage空间存储内容,因此我们只需快进到sstore指令即可.

找寻sstore
从Stack中可以看到0元素的起始地址是0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b,要在这个地址上存储的对象是就是0x3131313131313131313131313131313131313131313131313131313131313131,恰好就是name的值.

0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b就是Sha3(3).

构造成功的调用

首先起始地址是0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b,而sizeof(NameRecord)是2,注意不是64,因为EVM单位是32字节而不是字节
就很容易推算出来Index是

(0x10000000000000000000000000000000000000000000000000000000000000000-
0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b)/2
=0x1ed452f8b0d361ff8353039b6876926bcb1e6352e27d7e97d0ed74ddc84703d2

那么我们的调用参数就是

0x1ed452f8b0d361ff8353039b6876926bcb1e6352e27d7e97d0ed74ddc84703d2,"0x3131313131313131313131313131313131313131313131313131313131313131","0x692a70d2e424a56d2c6c27aa97d1a86395877b3a"

下图可以看到成功调用.
![调用结果]从一起“盗币”事件再谈合约安全问题第4张

四. 再一起来玩DVPgame

有了上面的思路相信大家就不会想着想法设法猜测lottery的x是多少了,直奔我们的fallback函数即可.

覆盖token

先通过guess函数把token设置为你自己事先部署的一份ERC20 Token,当然DVPgame就不会有任何这种新Token.

让hash(1)+msg.sender+x大于2**256,这个很容易满足吧.
然后把blockNum指定为你的token地址,相信他肯定会比当前的block.number大的.

悄悄告诉你hash(1)就是0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6,方便您试试.

随便转点以太坊给DVPgame

正常转账给DVPgame这个合约地址,无论多少都无所谓,反正最后都是会回到我们自己的账户上. 不过还是不要太多,万一有人捷足先登了呢.

看看别人怎么玩的

到底怎么玩我就不做了,因为已经有人玩过了,我也是事后诸葛亮. 链上直播看这里

五. 剩下的问题

如果你细心,就会发现我的例子中还有一个register函数没说.如果你自己尝试调用了,就会发现无论怎么调用都会成功,是不是颠覆了三观啊.
其实原因很简单,solidity中结构体默认是分配在storage空间中的(我也不知道为什么这么做,确实有点坑),而且这时候结构体的地址的起始地址就是0. 也就是说newRecord.name = _name;这句话在你不知不觉中就覆盖了unlocked.
说到这里,我还想说的是:如果你在写合约,请把solidity怎么工作的,搞清楚再动手

如果你够细心,至少register中的这个bug是可以避免的,因为solidity都警示你了.
![来自solidity的warn] (https://img2018.cnblogs.com/blog/124391/201811/124391-20181115120330641-1178298347.png)

solidity的任何warning都请不要忽略

六. 小测试工具

计算hash值的小工具

//Sha3 is short for Keccak256Hash
func Sha3(data ...[]byte) common.Hash {
	return crypto.Keccak256Hash(data...)
}

//BigIntTo32Bytes convert a big int to bytes
func BigIntTo32Bytes(i *big.Int) []byte {
	data := i.Bytes()
	buf := make([]byte, 32)
	for i := 0; i < 32-len(data); i++ {
		buf[i] = 0
	}
	for i := 32 - len(data); i < 32; i++ {
		buf[i] = data[i-32+len(data)]
	}
	return buf
}

func TestCalcHashSlot(t *testing.T) {
	i := big.NewInt(3)
	hash := Sha3(BigIntTo32Bytes(i))
	t.Logf("hash=%s", hash.String()) //0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b
	addr := common.Address{}
	fix := [32]byte{}
	copy(fix[12:], addr[:])
	hash = Sha3(addr[:])
	//addr=0x0000000000000000000000000000000000000000,it's hash=0x5380c7b7ae81a58eb98d9c78de4a1fd7fd9535fc953ed2be602daaa41767312a
	t.Logf("addr=%s,it's hash=%s", addr.String(), hash.String())
}

免责声明:文章转载自《从一起“盗币”事件再谈合约安全问题》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Oracle 11g、12C数据泵导入导出chrome 向群组中添加标签页下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

hash表的理解

哈希表 先从数组说起 任何一个程序员,基本上对数组都不会陌生,这个最常用的数据结构,说到它的优点,最明显的就是两点: 简单易用,数组的简易操作甚至让大多数程序员依赖上了它,在资源富足的情况下,我们甚至会无意识地忽略其它更适用的数据结构而使用数组(别说你没这么干过..)。 查找的快速性,数组中查找元素可以直接通过下标进行定位,速度快。 我在刚开始写程序...

面渣逆袭:HashMap追魂二十三问

大家好,我是老三。 HashMap作为我们熟悉的一种集合,可以说是面试必考题。简单的使用,再到原理、数据结构,还可以延伸到并发,可以说,就一个HashMap,能聊半个小时。 1.能说一下HashMap的数据结构吗? JDK1.7的数据结构是数组+链表,JDK1.7还有人在用?不会吧…… 说一下JDK1.8的数据结构吧: JDK1.8的数据结构是数组+链表+...

Android Verified Boot 2.0 AVB详解(基于Android P)

原文地址:https://android.googlesource.com/platform/external/avb/+/master/ 译文地址:https://blog.csdn.net/shangyexin/article/details/86649504 背景 在烧录系统镜像到设备的时候,发现烧写以后设备无限重启。请教了同事以后,发现需要输入下列...

各种字符串Hash函数(转)

/// @brief BKDR Hash Function /// @detail 本 算法由于在Brian Kernighan与Dennis Ritchie的《The C Programming Language》一书被展示而得 名,是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法(累乘因子为31)。 template...

分布式算法(一致性Hash算法)

转载:https://www.cnblogs.com/moonandstar08/p/5405991.html 一、分布式算法     在做服务器负载均衡时候可供选择的负载均衡的算法有很多,包括: 轮循算法(Round Robin)、哈希算法(HASH)、最少连接算法(Least Connection)、响应速度算法(Response Time)、加权法(...

Buffer Cache 原理

在将数据块读入到SGA中,他们的缓冲区被放置在悬挂散列存储桶的链表中(散列链),这种内存结构由大量 子cache buffers chains锁存器(也称为散列锁存器或CBC锁存器)保护。 Buffer Cache概述 Buffer Cache是SGA的一部分,Oracle利用Buffer Cache来管理data block,Buffer Cache的最...