Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to get the selector in a analysis module #1858

Open
huzhanchi opened this issue Jul 9, 2024 · 7 comments
Open

How to get the selector in a analysis module #1858

huzhanchi opened this issue Jul 9, 2024 · 7 comments

Comments

@huzhanchi
Copy link

Description

In the Defi protocol, it is very common to use "uniswapV3SwapCallback" like interface function to receive the callbak from LP, However some implementation of uniswapV3SwapCallback has no premission controll. Just as the uniswap official give the caution:
image
You can also check the background in my article here:
refer:https://medium.com/@zhanchihu/watch-your-back-while-you-want-to-do-aribitrage-with-uniswap-flashloan-f456e4f3e99d

Tests

todo

Implementation details

I want to implement a module to hook the CALL instruction, and give the constraint like

  1. the selector of message transaction call is "uniswapV3SwapCallback"
  2. ERC20 token could transfer to other address without any premission controlls

Now, my problem is how can I get the selector in the current CALL instruction. I have checked the "state.environment.calldata", and will to extract the selector, but in debug mode it look like this
image

Is there any convient way for me to get selector?

@norhh
Copy link
Collaborator

norhh commented Jul 9, 2024

EVM hashes the function uniswapV3SwapCallback and encodes it in the first 4 bytes of the call data.
You can find this hash using solidity by executing solc --hashes <filename> where the filename contains the functionuniswapV3SwapCallback along with its correct arguments.
Assume the hash you get from solc is something like fa461e33, the following will be the constraints:

  • calldata[0] == 250 (i.e., int("fa", 16) == 250)
  • calldata[1] == 70
  • calldata[2] == 30
  • calldata[3] == 51

@norhh
Copy link
Collaborator

norhh commented Jul 9, 2024

You can access the calldata by directly indexing the indices 0, 1, 2, and 3

@huzhanchi
Copy link
Author

You can access the calldata by directly indexing the indices 0, 1, 2, and 3
Hi norhh,thanks for reply. "state.environment.calldata" is the right way to get calldata in Mythril detect module ?
image
It is a BitVec

image

Maybe I got the the reason,in the attack process, "functionuniswapV3SwapCallback " should be callback from another contract. So SymbolicCalldata do not contain concrete data.

@norhh
Copy link
Collaborator

norhh commented Jul 9, 2024

It is not meant to contain concrete calldata. You have to add it as a constraint and run the solver to check if you are executing the desired function.

@huzhanchi
Copy link
Author

It is not meant to contain concrete calldata. You have to add it as a constraint and run the solver to check if you are executing the desired function.

I misunderstood the information given in the debug like "If(2_calldatasize <= 0, 0, 2_calldata[0])", and then I directly made an equal operation on the BitVec, it worked! Thanks for your patience.

@huzhanchi
Copy link
Author

huzhanchi commented Jul 10, 2024

Hi, another issue. I have write a module.there are two cases, one is detected as expected, another is not.
this module has two additional constraints:

  1. constraint the calldata's selector
  2. constraint the msg.sender shoud not be arbitrary

for the second constraint, mainly refer the function "is_unique_jumpdest" of "ArbitraryJump"

module core logic

def is_unique_sender(sender: BitVec, state: GlobalState) -> bool:
    try:
        # 0x fa 46 1e 33
        b0 = symbol_factory.BitVecVal(250, 256)
        b1 = symbol_factory.BitVecVal(70, 256)
        b2 = symbol_factory.BitVecVal(30, 256)
        b3 = symbol_factory.BitVecVal(51, 256)
 
        constraints = state.world_state.constraints + [state.environment.calldata[0] == b0,state.environment.calldata[1] == b1,state.environment.calldata[2] == b2,state.environment.calldata[3] == b3]
        model = get_model(constraints)
    except UnsatError:
        return True, constraints
    concrete_jump_dest = model.eval(sender.raw, model_completion=True)
    try:
        constraints += [symbol_factory.BitVecVal(concrete_jump_dest.as_long(), 256) != sender]
        model = get_model(constraints)
    except UnsatError:
        return True, constraints
    return False, constraints
def _analyze_state(self, state):
        state = copy(state)
        instruction = state.get_current_instruction()
        # 0x fa 46 1e 33
        b0 = symbol_factory.BitVecVal(250, 256)
        b1 = symbol_factory.BitVecVal(70, 256)
        b2 = symbol_factory.BitVecVal(30, 256)
        b3 = symbol_factory.BitVecVal(51, 256)
        sender = state.environment.sender
        
        (is_unique, constraints) = is_unique_sender(sender, state)
        if is_unique is True:
            return []

test case malicious contract

  • test in expection

Whether there is "require(msg.sender == lp_address)" or not, detect result in my expection. And also apply a modifier also work well too.

contract VulnCallback {

    address public lp_address = 0x1E227979f0b5BC691a70DEAed2e0F39a6F538FD5;
    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
            require(msg.sender == lp_address);
            payable(0x48Af6Ec8f9105c6a4a984ACa6D5E064629e3B7Db).transfer(address(this).balance);
    }
}
  • test not in expection

variable "pool0" could be controlled arbitrary from the malicious CALLER lp contract, at that point "msg.sender" could be any one.
The following test contract has no detected issue.

contract VulnCallback {
    address public lp_address = 0x1E227979f0b5BC691a70DEAed2e0F39a6F538FD5;
    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
             (address caller, address pool0, uint24 fee1) = abi.decode(data,(address, address, uint24));
             require(msg.sender == address(pool0), "not authorized");
            payable(0x48Af6Ec8f9105c6a4a984ACa6D5E064629e3B7Db).transfer(address(this).balance);
    }
}

@norhh
Copy link
Collaborator

norhh commented Jul 11, 2024

Mythril restricts the user addresses to the list of ACTORS, which you can find in mythril.laser.ethereum.transaction.symbolic
In this case you can either construct 3 different constraints pertaining to the 3 actors or construct one constraint pertaining to "SOMEGUY" from the class Actors

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants