智能合约-内联汇编操作码

opcode

内联汇编中使用的所有变量都是值类型

内联汇编中,所有的类型都是uint256

solidity中,内存数据的元素总是占用32bytes倍数的大小空间

先是存储数组的元素个数,然后再是实际元素

预留内存

Solidity 预留了四个 32 字节的槽,具有特定的字节范围

1
2
3
4
5
6
7
(包括端点)使用如下:
0x00 - 0x3f(64 字节):散列方法的暂存空间
0x40 - 0x5f(32 字节):当前分配的内存大小(又名空闲内存指针)
0x60 - 0x7f(32 字节):零槽(零槽用作动态的初始值内存阵列,并且永远不应该写入)
不能保证之前没有使用过内存,因此你不能假设它的内容是零字节。没有内置的释放机制或释放分配的内存。

空闲内存指针将立即开始指向保留内存插槽之后的地址,指向0x80。尽管任何地址都可以访问(不仅仅是32字节的块),但Solidity总是以32字节块的形式使用内存,你也应该这样做:不要忘记为写入的每个新块增加32个指针!

比如下面的方法

1
2
3
4
5
6
7
8
9
function f (){
assembly {
let freemem_pointer := mload(0x40)
mstore(add(freemem_pointer,0x00),"36e5236fcd4c61044949678014f0d085")
mstore(add(freemem_pointer,0x20),"36e5236fcd4c61044949678014f0d086")
let arr1:= mload(freemem_pointer) //read first string
mstore(add(freemem_pointer,0x40),arr1) //s3
}
}

智能合约-内联汇编操作码

前面的代码在空闲内存空间中存储了两个字符串(两个内存字,每个 32 字节)。目标内存地址是通过将第一个偏移量 0 字节和第二个偏移量 0x20 添加到空闲内存指针地址(位于内存地址 0x40 中)来获得的。
在 EVM 中,内存中的前 6 个字被保留,0x40-0x50 内存字被分配给空闲内存指针。

mload&add

  • mem[p…(p+32))

  • add(x, y) :x + y

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 下面的 _input 作为形参,在内联汇编中,它是一个address,而不是类似指针的引用类型
function assemblyKeccak (bytes memory _input) public pure returns (bytes32 x) {
assembly {
x := keccak256(add(_input, 0x20), mload(_input))
}
}

// mload(_input): 获得_input这个地址的32字节的实际数据,即数组的元素个数
// 假如我们的 _input 是33字节的数据,那么内存的结构布局如下:
// memory[_input] : Length 0x31(33)
// memory[add(_input,0x20)]: 0x0101010101010101010101010101010101010101010101010101010101010101
// memory[add(_input,0x40)]: 0x0100000000000000000000000000000000000000000000000000000000000000
// 注意:_input是一个address,告诉在哪可以找到memory[_input]
// add(_input, 0x20): 0x20是32,即跳过32字节的数据,即跳过数组的长度,直接获得数组的实际数据的address,可以把它当作_input[0]的address

sload&shr&and

  • sload(x):获取插槽x的数据

  • shr(x, y) :将 y 逻辑右移 x 位

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract test{
uint256 public xxxx = 10 & 3; //1010 & 0011 = 0010 = 2
uint256 public yyyy = 18 & 0xf; //1 0010 & 1111 = 0010 = 2

struct S {
uint128 a;
uint128 b;
}
S test1;
function x() public returns (uint a, uint b, uint c){
test1 = S(5,10);
// slot 0: 0x0000000000000000000000000000000a00000000000000000000000000000005
// 因为是数值类型,所以从低位排起
// 将结果3402823669209384634633746074317682114565转为16进制:a00000000000000000000000000000005
assembly {
a:=sload(0) //3402823669209384634633746074317682114565
b:=sload(1) //0
c:=sload(0) //3402823669209384634633746074317682114565
}
}

function y() public returns (uint a, uint b, uint c, uint d, uint f, uint g){
test1 = S(5,10);
assembly {
let w := sload(0)
a := and(w, 0xffffffffffffffffffffffffffffffff) //从低位开始按位与
b := shr(128, w) //将w逻辑右移128位,即变成了0x000000000000000000000000000000000000000000000000000000000000000a
}
}//a=5,b=10

}

.slot和.offset

1
2
// _left是形参
let rightAndLeftValues := sload(_left.slot) // 获取_left变量的所在的slot
1
2
3
4
5
6
    uint248 _right; //状态变量
uint8 _left; //状态变量

// _left.offset = 31,即_left的实际数据是在其slot中的31bytes 偏移量之后
// 31 * 8 = 248 ,即逻辑右移了258位,把_right移除了
let leftValue := shr(mul(_left.offset, 8), rightAndLeftValues)

mstore

  • mstore(p,v): mem[p..(p+32)] := v

也就是说,将v的值赋给address=p之后的32字节

1
2
3
4
function toBytes(uint256 x) returns (bytes b) {
b = new bytes(32);
assembly { mstore(add(b, 32), x) }
}
  1. 在 Solidity 中,bytes 是一个动态大小的字节数组:b = new bytes(32);,这将创建一个名为b的变量,类型为bytes。我们现在知道b将被视为一个数组。

  2. 在 Solidity Assembly 中,变量是指向内存地址的指针:我们知道b会指向它的内存地址。

  3. 内存和存储以 32 字节为单位进行管理。

  4. 数组的第一个内存块存储该数组的长度:通过 3 和 4 我们现在知道b将指向它的长度,并且只有 32 个字节之后是b值开始的内存地址。

calldatacopy

  • calldatacopy(t, f, s):从位置f的calldata复制s字节到位置t的内存中

下面的例子:得到calldata中的第一个形参数据

1
2
3
4
5
6
7
8
function parseMsgData() public view returns (bytes32 _address){
bytes32 _address;
assembly {
calldatacopy(0x0, 4, 32)
_address := mload(0x0)
}
return _address;
}

extcodesize

  • extcodesize(a):地址为a的代码的大小

下面的方法确定了一件事:输入的形参这个地址,要么是EOA地址,要么会被构造器攻击

1
2
3
4
5
function isEOAorContract(address addr) returns (bool) {
uint size;
assembly { size := extcodesize(addr) }
return size = 0;
}

add&mod&addmod

  • addmod:此操作的所有中间计算均不受 2^256 模的影响。这意味着您可以在 2^256 位限制以上进行算术运算,因为它们的实现(opAddmod,opMulmod)实际上在内部对大整数进行操作并且只返回结果。由于模值不能超过 2^256,因此结果不会溢出。

1
2
3
4
5
6
7
8
function classicAddMod() public view returns (uint256 rvalue) {

uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;

assembly {
rvalue := mod(add(MAX_INT, 1), 10)
}
}//return 0

先add,变成0,取模还是0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function opAddMod_1() public view returns (uint256 rvalue) {

uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;

assembly {
rvalue := addmod(MAX_INT, 1, 10)
}
}// return 6

function opAddMod_2() public view returns (uint256 rvalue) {

uint256 xxx = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe;

assembly {
rvalue := addmod(xxx, 1, 10)
}
}// return 5

opAddMod_1:先add,变成2^256,然后mod 10,等于6


招新小广告

ChaMd5 Venom 招收大佬入圈

新成立组IOT+工控+样本分析 长期招新

欢迎联系[email protected]



智能合约-内联汇编操作码

原文始发于微信公众号(ChaMd5安全团队):智能合约-内联汇编操作码

版权声明:admin 发表于 2023年5月13日 上午8:32。
转载请注明:智能合约-内联汇编操作码 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...