一种绕过管理员权限调用智能合约view函数的小技巧

Posted Zero_Nothing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一种绕过管理员权限调用智能合约view函数的小技巧相关的知识,希望对你有一定的参考价值。

在部分以太坊智能合约中,有时可以看到某些view函数被限定了管理员调用,以阻止其它人查看。其实这个限定基本上是无效的,本文介绍一下绕过这个限定调用相应的view函数的小技巧。注意:我们这里仅限view类型的函数(非改变状态的交易)。

一、介绍

有的时候,智能合约开发者由于需求,会将部分数据设置为私有类型,并且限定了只有管理员才能查看(例如一个随机数的种子,或者管理员地址)。这时我们又想知道这个数据怎么办?正常的函数调用肯定是无法验证调用者地址验证的,这里,我们就得想想办法了,利用ethers.js框架中调用智能合约时指定的overrides对象可以轻松绕过地址验证,从而将不可见信息变成可见信息。

当然有读过我上一篇《使用ethers.js直接读取智能合约中插槽内容》的读者会说到,我们可以直接读取插槽信息!Very Good!我们当然要利用这个技巧,但是有部分数据不方便使用插槽直接读取(例如mapping),有些数据还要经过复杂的计算和第三方调用,此时利用读取插槽来获取足够多的信息最终得到结果就显得有些笨拙了,我们需要一种更简单的方式。

二、编写测试合约

我们编写一个测试合约,源码如下:

pragma solidity =0.6.6;

contract ViewTest {
    address private owner;
    uint private _seed;
    mapping(address => uint) private luckyNum;
    mapping(address => uint) private luckyNumTwo;

    modifier onlyOwner() {
        require(msg.sender == owner, "no access");
        _;
    }

    constructor() public {
        owner = msg.sender;
        _seed = block.number;
        luckyNum[msg.sender] = uint(msg.sender) % 1000;
        luckyNum[address(0)] = 666;
        luckyNumTwo[msg.sender] = uint(msg.sender) % 10000;
        luckyNumTwo[address(0)] = 6666;
    }

    function getMyAddress() external view returns(address) {
        return msg.sender;
    }

    function getLuckyNum() external view returns(uint) {
        return luckyNum[msg.sender];
    }

    function getLuckyNumTwo(address user) external view onlyOwner returns(uint) {
        return luckyNumTwo[user];
    }

    function getSeed() external view onlyOwner returns(uint) {
        return _seed;
    }

    function setSeed(uint seed) external onlyOwner returns(bool) {
        _seed = seed;
        return true;
    }
}

该合约已经部署在Kovan测试网上,合约地址为0x2B4FCa37e5FE788ecE0BFC997a91a8993C176Ed5,并且已经通过浏览器验证,使用下面的链接可以直接查看代码:
https://kovan.etherscan.io/address/0x2B4FCa37e5FE788ecE0BFC997a91a8993C176Ed5#code

测试合约很简单,主要是将部分私有状态变量的读取(view类型函数)限定为owner调用(作为对比,部分view类型函数未限定调用权限),注意,这个owner本身也是私有的。

三、overrides对象

ethers.js框架中,调用合约的函数时可以添加一个自定义的overrides对象,这个对象其中有这么一个属性:from,用来指定调用者的地址。如果我们不写这个overrides对象,这个调用者地址就是零地址(当然,如果在ether.js中将合约绑定了钱包,默认调用者地址就是钱包地址)。我们平常在调用合约时指定的gasLimitgasPrice也是属于这个overrides对象的,from字段我们很少用到。

于是,如果我们得到了owner地址,就可以将from字段设置成owner地址来指定调用者地址。乍一看,我们没有别人的私钥怎么能指定调用者为别人呢?记住,我们这里的主题说的是调用view函数(并不是一个改变状态的交易),是不需要签名的,因此,也就可以通过验证。

四、编写测试脚本

我们编写一个viewTest.js来进行上述技巧的验证。

//导入ethers
const {ethers,utils} = require("ethers")  
//连接infura节点
const url = "https://kovan.infura.io/v3/your_infura_key"
const provider = new ethers.providers.JsonRpcProvider(url,"kovan")
//创建合约实例
const address = "0x2B4FCa37e5FE788ecE0BFC997a91a8993C176Ed5"
const abi = [
    "function getLuckyNum() view returns(uint)",
    "function getSeed() view returns(uint)",
    "function getMyAddress() view returns(address)",
    "function getLuckyNumTwo(address user) view returns(uint)",
]
const contract = new ethers.Contract(address,abi,provider)

//获取不指定调用者账号时的地址,为零地址
async function getMyAddress() {
    const my_address = await contract.getMyAddress()
    console.log(my_address === ethers.constants.AddressZero)  //true
}

//获取零地址对应幸运数字,应该为666
async function getLuckyNum() {
    const n = await contract.getLuckyNum()
    console.log("luckyNum:", n.toString())   //666
}

//获取账号对应的幸运数字,因为权限限定,会报错。
async function getLuckyNumTwo() {
    try{
        const n = await contract.getLuckyNumTwo(ethers.constants.AddressZero)
        console.log("luckyNumTwo:", n.toString())   
    }catch(e) {
        console.log("getLuckyNumTwo error")
    }
}

//使用管理员地址去调用
async function getLuckyNumTwoByOwner(owner_address) {
    const n = await contract.getLuckyNumTwo(owner_address,{
        from:owner_address
    })
    console.log("luckyNumTwo:", n.toString())  
}

//获取私密数字_seed,因为权限限定,应该报错。
async function getSeed() {
    try {
        const seed = await contract.getSeed()
        console.log("seed:",seed.toString())
    }catch(e) {
        console.log("getSeed error")
    }
}

//使用管理员地址去调用
async function getSeedByOwner(owner_address) {
    const seed = await contract.getSeed({
        from:owner_address
    })
    console.log("seed:",seed.toString())  //blockNumber 25753852
}

//直接从插槽获取owner地址
async function getOwnerBySlot() {
    const owner_info = await provider.getStorageAt(address ,0)  //owner在零插槽
    const owner_address = utils.getAddress("0x" + owner_info.substring(26))
    console.log("owner_address:",owner_address)
    return owner_address
}

//直接从插槽获取seed值,绕过onlyOwner
async function getSeedBySlot() {
    const seed_info = await provider.getStorageAt(address ,1)  //seed在1插槽
    const seed = ethers.BigNumber.from(seed_info)
    console.log("seed:", seed.toString())  //blockNumber 25753852
}

//获取其它插槽信息,主要是演示不是所有数据都方便通过插槽获取(比如中间有复杂的计算,比如map等)
async function getOtherSlotInfo() {
    for(let i=2;i<=10;i++) {
        let info = await provider.getStorageAt(address ,i)
        console.log(info)
    }
}

async function start() {
    await getMyAddress() //得到未指定账号时的调用者地址,为零地址
    await getLuckyNum()  //调用未限定权限的view函数,结果为666
    await getSeed()   //调用限定权限的view函数,结果为error
    await getSeedBySlot()  //直接读取插槽信息获取_seed值,结果为 25753852
    let owner_address = getOwnerBySlot() //直接读取插槽信息获取管理员地址
    await getSeedByOwner(owner_address) //指定调用者为owner时调用限定权限的view函数,成功,结果为 25753852
    await getLuckyNumTwo() //调用指定权限的view函数,会出错
    await getLuckyNumTwoByOwner(owner_address) //使用管理员调用指定权限的view函数,成功
    await getOtherSlotInfo()  //连续9个空字节
}

start()

最后输出的结果为:

➜  local_tools node viewTest.js 
true
luckyNum: 666
getSeed error
seed: 25753852
owner_address: 0xDD55634e1027d706a235374e01D69c2D121E1CCb
seed: 25753852
getLuckyNumTwo error
luckyNumTwo: 8587
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000

运行上述脚本需要将your_infura_key进行替换。什么?还没有,那就去https://infura.io/申请一个吧。

小提示:

本脚本中使用的abi为人类易读格式abi,即:Human-Readable Contract ABIs。详细文章见https://blog.ricmoo.com/human-readable-contract-abis-in-ethers-js-141902f4d917?gi=fcdcdcacfce9

脚本上已经有注释了,这里再稍微解释一下:

  1. 首先,我们验证了当不指定overrides.from时,调用者地址为零地址(getMyAddress函数)。
  2. 我们调用了一个不需要权限的view函数(getLuckyNum),输出正常。
  3. 我们调用了一个需要权限的view函数(getSeed),会报错。
  4. 我们使用插槽直接读取了私有变量的值(getSeedBySlot),成功。
  5. 我们使用插槽获取管理员地址(getOwnerBySlot),成功。
  6. 指定管理员地址后我们再次调用getSeed函数,成功。
  7. getLuckyNumTwo、getLuckyNumTwoByOwner 与 getOtherSlotInfo 是为了演示不是所有信息都能方便的直接从插槽中读取,比如对于一个mapping,你就不知道它的插槽位置在哪,因此,使用overrides.from总是有必要的。
  8. 上面还演示了一个技巧,如果不知道管理员地址,首先通过插槽获取管理员址。在本例中管理员地址插槽位置为0,在其它合约中可能不同。

五、其它

希望这篇文章能对学习以太坊和智能合约开发的人有所帮助,特别是那些习惯指定onlyOwner来限定别人读取view函数的,是很容易被绕过这个限制的。

虽然我们也能制造一些障碍,例如使用一个mapping来记录所有的operator,而那些函数限定operator调用。上面提到过,mapping类型是不方便通过插槽直接读取相应的值的。但是,operator总得有操作吧,通过观察合约的交易记录(或者该交易的堆栈追踪),很容易判断出谁是operator了。因此,总之一句话,在以太坊上,一切都是透明的(至少大体上是这样)!

再次声明:本文只针对智能合约的view(pure)类型函数。

以上是关于一种绕过管理员权限调用智能合约view函数的小技巧的主要内容,如果未能解决你的问题,请参考以下文章

智能合约实战 solidity 语法学习 03 [ 函数修饰符 view public private constant payable ]

智能合约实战 solidity 语法学习 04 [ 函数修饰符 view public private constant payable ] 附代码

基于以太坊的智能合约开发教程Solidity 继承与权限

智能合约重构社会契约以太坊总结

如何开发编译部署调用智能合约

一学就会,手把手教你用Go语言调用智能合约