以太坊的消息签名 CTF工具汇总

  • BY_DLIFE
  • 更新于 2024-04-30 11:38
  • 阅读 375

📌靶场刷题遇得到很多关于验证签名的题,在这里汇总一下,消息签名的工具和方法。

前言

📌靶场刷题遇得到很多关于验证签名的题,在这里汇总一下,消息签名的工具和方法。

1. 采用 web3.py

1.1 适用于本地测试

这是不符合当前 以太坊 规定的签名,即未加入\x19Ethereum Signed Message:\n32

适合用于平时在本地复现靶场的简单使用。

### 这是没加 \x19Ethereum Signed Message:\n32
from eth_account import Account
from web3 import Web3

message = ""
privatekey = ""
messagehash = Web3.keccak(text=message)
signMessage = Account.signHash(message_hash=messagehash, private_key=privatekey)

print("message =", message)
print("message's hash =",messagehash.hex())
print("v =", Web3.to_hex(signMessage.v))
print("r =", Web3.to_hex(signMessage.r))
print("s =", Web3.to_hex(signMessage.s))
print("signature =", Web3.to_hex(signMessage.signature))

1.2 适用于测试网

如下是遵循EIP191协议的签名规则的代码,即加入\x19Ethereum Signed Message:\n32

如下这两种方法和metamask的签名结果一样。

但是如下这里个并不是按照如下的计算方式:

        bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        bytes32 result = keccak256(abi.encodePacked(prefix, hash));
# 加入了 \x19Ethereum Signed Message:\n32
from web3.auto import w3
from eth_account.messages import encode_defunct

msg = ""
private_key = ""
message = encode_defunct(text=msg)
signed_message =  w3.eth.account.sign_message(message, private_key=private_key)
print("message =", msg)
print("messageHash =", w3.to_hex(signed_message.messageHash))
print("r =", w3.to_hex(signed_message.r))
print("s =", w3.to_hex(signed_message.s))
print("v =", w3.to_hex(signed_message.v))
print("signature =", w3.to_hex(signed_message.signature))

from web3 import Web3, HTTPProvider
from eth_account.messages import encode_defunct

# 私钥
private_key = ""
rpc = 'https://rpc.ankr.com/eth' # 遵从主网规则
w3 = Web3(HTTPProvider(rpc))

msg = ""

#构造可签名信息
message = encode_defunct(text=msg)

# sign
signed_message = w3.eth.account.sign_message(message, private_key=private_key)

print("msg =", msg)
print("msgHash =", w3.to_hex(signed_message.messageHash))
print("r =", w3.to_hex(signed_message.r))
print("s =", w3.to_hex(signed_message.s))
print("v =", w3.to_hex(signed_message.v))
print("signature = ", w3.to_hex(signed_message.signature))

代码结果运行图

image.png

metamask签名结果图:

image.png

2. 采用web3.js

web3.js的版本为:"version": "1.8.0"

这个方法就很牛皮了:

  • 如果输入的data是string类型的:那么ta的运算结果和metamask的结果一样

image.png

  • 如果输入的data是hash:那么ta的处理方式就是如下
        bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        bytes32 result = keccak256(abi.encodePacked(prefix, hash));

remix代码结果:

image.png

web3js代码结果:

image.png

代码:

var Web3 = require('web3');
var web3 = new Web3(Web3.givenProvider);

let dataHash = "";
let privateKey = ""
let sign = web3.eth.accounts.sign(dataHash, privateKey);

console.log(sign);

3. 采用 ethers.js

版本为:^6.2.3。

该脚本的签名结果和方式和metamask也是一样,遵循EIP191,不会像web3js那样,输入为hash时,不遵守

        bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        bytes32 result = keccak256(abi.encodePacked(prefix, hash));
import { ethers } from "ethers";

const RPC = "";

const provider = new ethers.JsonRpcProvider(RPC);

const privateKey = "";
const wallet = new ethers.Wallet(privateKey, provider);

const message = "";
const messageHash = ethers.hashMessage(message);

const signature = await wallet.signMessage(message);
console.log(`message = ${messageHash}`);
console.log(`signatrue = ${signature}`);

4. 采用ethereum.js

这是未遵循EIP191协议的签名方式

const ethereumjsUtil = require('ethereumjs-util');

// 要签名的消息
const message = 'stage1';

// 私钥(注意:这只是一个示例私钥,不应该在实际项目中使用)
const privateKey = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex');

// 生成消息的 Keccak-256 哈希
let buffer_message = Buffer.from(message); // 将message转成字节流
const messageHash = ethereumjsUtil.keccak256(buffer_message);

// 使用私钥对消息哈希进行签名
const signature = ethereumjsUtil.ecsign(messageHash, privateKey);

// 将签名结果进行格式化
const formattedSignature = {
  v: signature.v,
  r: signature.r.toString('hex'),
  s: signature.s.toString('hex')
};

console.log('Message:', message);
console.log('Message Hash:', messageHash.toString('hex'));
console.log('Signature:', formattedSignature);

5. 签名和消息还原地址

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol";

contract SignMessage {

    using ECDSA for bytes32; 

    function verifyMessage(string memory message, bytes memory signature) public view  returns(address, bool) {
        //hash the plain text message
        bytes32 messagehash =  keccak256(bytes(message));

        address signeraddress = messagehash.recover(signature);

        if (msg.sender==signeraddress) {
            //The message is authentic
            return (signeraddress, true);
        } else {
            //msg.sender didnt sign this message.
            return (signeraddress, false);
        }
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SignMessage {

        // @dev 从_msgHash和签名_signature中恢复signer地址
    function recoverSigner(bytes32 _msgHash, bytes memory _signature) public pure returns (address){
        // 检查签名长度,65是标准r,s,v签名的长度
        require(_signature.length == 65, "invalid signature length");
        bytes32 r;
        bytes32 s;
        uint8 v;
        // 目前只能用assembly (内联汇编)来从签名中获得r,s,v的值
        assembly {
            /*
            前32 bytes存储签名的长度 (动态数组存储规则)
            add(sig, 32) = sig的指针 + 32
            等效为略过signature的前32 bytes
            mload(p) 载入从内存地址p起始的接下来32 bytes数据
            */
            // 读取长度数据后的32 bytes
            r := mload(add(_signature, 0x20))
            // 读取之后的32 bytes
            s := mload(add(_signature, 0x40))
            // 读取最后一个byte
            v := byte(0, mload(add(_signature, 0x60)))
        }
        // 使用ecrecover(全局函数):利用 msgHash 和 r,s,v 恢复 signer 地址
        return ecrecover(_msgHash, v, r, s);
    }
}

6. 实现同一私钥,同一消息,不同签名

6.1通过ethereum.js实现

这需要修改ethereum.js的源码,版本为:"version": "7.1.5"

修改一:node_modules\@types\secp256k1\index.d.ts

// 源代码
noncefn?: ((message: Uint8Array, privateKey: Uint8Array, algo: Uint8Array | null, data: Uint8Array | null, attempt: number) => Uint8Array)

这里可以看到data的值被写死了,被默认写成null,所以导致了生成的 options的值是new Uint8Array(0),这就影响了生成的签名是唯一的"错觉"。如何将这个默认值给去掉,在调用的时候传入随机的options(通过生成随机的Uint8Array数组实现),所以将这里的代码修改为

// 去掉data的默认值
noncefn?: ((message: Uint8Array, privateKey: Uint8Array, algo: Uint8Array | null, data: Uint8Array, attempt: number) => Uint8Array) | undefined;

修改二:node_modules\ethereumjs-util\dist\signature.js

// 源码
const { signature, recid: recovery } = (0, secp256k1_1.ecdsaSign)(msgHash, privateKey);

image.png

可以看到这里没有传入option,即使用了源码的默认option(null),因为上一步修改了option的默认值,所以这里可以传参了,可以引入crypto库,随机生成Uint8Array数组,这样就可以随机生成签名,所以将这里的代码修改为:

const crypto = require('crypto'); // 先导库
const { signature, recid: recovery } = (0, secp256k1_1.ecdsaSign)(msgHash, privateKey, {data:crypto.randomBytes(32)});  // 生成随机数组

在 Node.js 中,crypto.randomBytes()方法是一个常见的随机数生成方法

综上,经过两次修改便可以实现使用同一私钥,对同一消息,进行签名可以得到不同的签名值

举例:

签名脚本,使用私钥1,对消息stage1进行签名

const ethereumjsUtil = require('ethereumjs-util');

// 要签名的消息
const message = 'stage1';

// 私钥(注意:这只是一个示例私钥,不应该在实际项目中使用)
const privateKey = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex');

// 生成消息的 Keccak-256 哈希
let buffer_message = Buffer.from(message); // 将message转成字节流
const messageHash = ethereumjsUtil.keccak256(buffer_message);

// 使用私钥对消息哈希进行签名
const signature = ethereumjsUtil.ecsign(messageHash, privateKey);

// 将签名结果进行格式化
const formattedSignature = {
v: signature.v,
r: signature.r.toString('hex'),
s: signature.s.toString('hex')
};

console.log('Message:', message);
console.log('Message Hash:', messageHash.toString('hex'));
console.log('Signature:', formattedSignature);

运行结果:

image.png

可以从结果中看到,生成的签名值不一样,到合约中验证:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Verify {
 // private key = 0x1
 function verify(uint8 v, bytes32 r, bytes32 s) public  {
     require(ecrecover(keccak256("stage1"), v, r, s) == 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, "who are you?");
 }
}

运行结果:

image.png

从结果中可以看到,生成的这些签名都可以通过验证。

6.2 通过加密库实现

这种方法不能准确获取signature,因为最后的v无法确定,只能猜测,但是v只能是 0x1b 或者 0x1c所以猜对的可能性为0.5,但是也可以实现生成不同签名的功能。

const secp256k1 = require('secp256k1');
const { randomBytes } = require('crypto');

/**
 * 
 * @param {*} numSignatures :生成签名数量
 * @param {*} PKey :私钥
 * @param {*} MessageHash :消息的hash值,不带 `0x`
 * 这种方法不能准确获取signature,因为最后的v无法确定,只能猜测,但是v只能是 0x1b 或者 0x1c所以猜对的可能性为0.5
 */
function batchSign(numSignatures, PKey, MessageHash) {
    const privateKey = Buffer.from(PKey, 'hex');
    const messageHash = Buffer.from(MessageHash, 'hex');
    for (let i = 0; i < numSignatures; i++) {
        const { signature } = secp256k1.ecdsaSign(messageHash, privateKey, { data: randomBytes(32) });
        signatureBytes = Buffer.from(signature)
        signatureHex = signatureBytes.toString('hex');

        const r = signatureHex.slice(0, 64);
        const s = signatureHex.slice(64);
        console.log(`Signature ${i + 1}:`);
        console.log("Signature:",`0x${signatureHex}`);
        // console.log('Signature (v):', v);
        console.log('Signature (r):', `0x${r}`);
        console.log('Signature (s):', `0x${s}`);
        console.log('Signature (s):',"1b or 1c");
        console.log('----------------------');
    }
}

//test
batchSign(5, "0000000000000000000000000000000000000000000000000000000000000001", "8252a7072c69c0cdba0c0bc059898f7992314306b3f0845bbb76593da6b98311")
点赞 3
收藏 3
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
BY_DLIFE
BY_DLIFE
0x994d...4240
立志成为一名智能合约安全审计师。文章都是我的个人理解,如果有不对的地方欢迎在评论区指出来。