Đôi chút về Mnemonic (Recovery Phrase)
Giới thiệu:
Ý tưởng được xuất phát từ khi tạo các ví trên Trust wallet, Metamask, Exodus wallet, chúng ta sẽ phải tạo 12 cụm từ gọi là khoá bí mật (recovery pharse), vậy 12 cụm từ này có thực sự an toàn hay không, chúng ta cùng tìm hiểu nhé.
- Mnemonic recovery phrase* (còn được gọi là mnemonic seed phrase, recovery seed, mnemonic phrase) là một chuỗi các từ được sử dụng để khôi phục và tái tạo một ví tiền điện tử hoặc một tài khoản blockchain. Thường gồm 12, 18 hoặc 24 từ, các từ này được chọn từ một danh sách các từ chuẩn được xác định trước (thông thường là từ danh sách BIP39). Danh sách này gọi là bip39 word list gồm 2048 từ, các bạn có thể xem ở đây: BIP39 Word List.
- Bài viết này để không đề cập đến việc các địa chỉ ví được tạo ra như thế nào mà chỉ cố xác định cái gọi là Mnemonic kia có thực sự an toàn, bởi lẽ 12 cụm từ này nghe có vẻ đơn giản nhỉ dò tí là ra mà 😆 . Chi tiết việc tạo ra mnemonic các bạn có thể tìm hiểu tại đây: Mnemonic
- Nếu lấy ngẫu nhiên 12, 18 hoặc 24 cụm từ này trong Bip39 Word Lists mà trùng với 1 ví có sẵn thì chắc giàu to đúng không nào 🤣 OK để mình thử dùng kinh nghiệm nodejs 2 ngày của mình để làm thử. Có 2 hướng tiếp cận bài toán này ở đây:
- Chọn ngẫu nhiên 12 cụm từ trong danh sách 2048 từ, tạo ra 1 địa chỉ ví, sau đó check số dư của địa chỉ ví này, nếu số dư ETH lớn hơn 0 thì log private key + address.
- Chọn ngẫu nhiên 12 cụm từ trong danh sách 2048 từ, tạo ra 1 địa chỉ ví, dò trong danh sách các địa chỉ ví có sẵn (ví có sẵn ở đây mình tham vọng là top 10,000 địa chỉ ví có số dư lớn nhất Top Accounts ) nếu trùng thì log private key + address.
Cách 1: Ở cách này mình sẽ tạo ra 1 chương trình lấy random 12 từ sau đó check số dư của ví được tạo ra bởi 12 từ này
- Tạo 1 cái worker.js
var fs = require("fs")
const { ethers } = require("ethers")
var tries = 0, hits = 0
const delay = time => new Promise(res => setTimeout(res, time));
var words = fs.readFileSync("bip39.txt", { encoding: 'utf8', flag: 'r' }).replace(/(\r)/gm, "").toLowerCase().split("\n")
function gen12(words) {
var n = 12
var shuffled = words.sort(function () { return .5 - Math.random() })
return (shuffled.slice(0, n)).join(" ");
}
console.log("starting....")
async function doCheck() {
tries++
try {
var wall = ethers.Wallet.fromMnemonic(gen12(words))
fs.appendFileSync('hits.txt', wall.address + "," + wall.privateKey + "\n")
hits++
process.stdout.write("+")
} catch (e) { }
await delay(0)
process.stdout.write("-")
doCheck()
}
doCheck()
Nhiệm vụ của cái worker này là. lấy random 12 từ trong file "bip39.txt" (bip39 word list) tạo ra private key và address từ mnemonic này, nếu valid thì lưu vào file hits.txt. thư viện ethers giúp chúng ta tạo ra cái private key + address này. Lưu vào file txt nghe có vẻ củ chuối nhỉ nhưng nó là cách đơn giản nhất so với kinh nghiệm node js 2 ngày của mình 😂. Ở cách 2 chúng ta sẽ lưu xuống DB nhé. 2. Tạo file index.js để chạy cái worker này:
const { fork } = require("child_process");
var devnull = require("dev-null")
const { program } = require('commander');
var tries = 0, hits = 0
var children = []
program
.option("-c, --count <number>", "number of processes")
var options = program.parse().opts()
const count = parseInt(options.count) || 6
console.log(`starting ${count} processes`.yellow)
for (var i = 0; i < count; i++) {
children[i] = fork("worker.js", [], { detatched: false, stdio: "pipe" })
children[i].stdout.setEncoding('utf8')
children[i].stdout.on("data", (data) => {
if (data == "+") {
hits++
tries++
} else {
tries++
}
}).pipe(devnull())
}
process.on("SIGTERM", () => {
children.forEach((val) => {
val.kill("SIGTERM")
})
})
console.log("all processes started".green)
import('log-update').then(mod => {
const frames = ['-', '\\', '|', '/'];
var index = 0;
setInterval(() => {
const frame = frames[index = ++index % frames.length];
mod.default(`${frame} tries: ${tries}; hits: ${hits} ${frame}`);
}, 1);
});
- Tạo file check_balances.js để check số dư
const fs = require("fs");
const ethers = require("ethers");
require("colors");
const providerUrl = "wss://mainnet.infura.io/ws/v3/YOUR_API_KEY";
const addresses = fs
.readFileSync("hits.txt", "utf8")
.split("\n")
.map((val) => val.split(","));
(async () => {
const provider = new ethers.providers.WebSocketProvider(providerUrl);
// Define a function to check the balance of a single address
async function checkBalance(address, privateKey) {
try {
const balance = await provider.getBalance(address);
if (balance.gt(0)) {
console.log(address.bgGreen.black, balance.toString().bgGreen.black);
console.log("Private Key: ".yellow, privateKey);
fs.appendFileSync("pass.txt", address + "," + privateKey + "\n");
} else {
console.log(address, 0);
}
} catch (err) {
console.error(`Error checking balance for ${address}: ${err.message}`);
}
}
const promises = addresses.map(([address, privateKey]) =>
checkBalance(address, privateKey)
);
// Execute promises concurrently and wait for all of them to complete
await Promise.all(promises);
provider.connection._websocket.close(); // Disconnect the provider when done
})();
Ở đây mình sử dụng Infura để check số dư ví, Infura cho phép gửi tối đa 100k request/day (miễn phí), và bạn có thể cài đặt được max rate limit (số lượng request gửi được trong 1s), Ngoài ra còn có Alchemy cũng có thể sử dụng để check số dư ví nhưng sẽ bị giới hạn max rate limit. Các bạn có thể lên trang chủ đăng kí tài khoản rồi tạo API_KEY để sử dụng nhé. Đây là giao diện khi tạo project trên Infura thành công.
Kết quả sau khi chạy :
Cách 2: cách này cũng tạo ra mnemonic nhưng sử dụng sql query giúp giải quyết được vấn đề Max rate limits của cách 1 bởi vì gửi 100k request/day có vẻ hơi ít so với số lượng địa chỉ ví cần check để tìm ra được gì đó
- Đầu tiên đưa bip39 wordlists vào DB, ở đây mình sử dụng SQLite
const colors = require('colors');
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('addresses.db');
const fs = require("fs");
const { EthHdWallet } = require('eth-hd-wallet');
async function initBip39Data() {
try {
await createBip39Table();
const bip39Data = fs.readFileSync('bip39.txt', 'utf8').split('\n');
const insertPromises = bip39Data.map(async (data) => {
await insertBip39Mnemonic(data);
});
await Promise.all(insertPromises);
console.log('BIP39 data initialization complete.');
} catch (error) {
console.error('Error initializing BIP39 data:', error);
}
}
async function createBip39Table() {
return new Promise((resolve, reject) => {
db.run('CREATE TABLE IF NOT EXISTS bip39_data(mnemonic TEXT PRIMARY KEY)', (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
async function insertBip39Mnemonic(mnemonic) {
return new Promise((resolve, reject) => {
db.run('INSERT OR IGNORE INTO bip39_data(mnemonic) VALUES(?)', [mnemonic], function (err) {
if (err) {
reject(err);
} else {
if (this.changes === 0) {
console.log(`Mnemonic '${mnemonic}' already exists in the database.`);
} else {
console.log(`Mnemonic '${mnemonic}' inserted into the database.`);
}
resolve();
}
});
});
}
- Tiếp đến là đưa danh sách top accounts ETH Wallet vào db
async function initAddressData() {
try {
await createAddressTable();
const addresses = fs.readFileSync('address.txt', 'utf8').split('\n');
const insertPromises = addresses.map(async (data) => {
await insertAddress(data);
});
await Promise.all(insertPromises);
console.log('Database initialization complete.');
} catch (error) {
console.error('Error initializing database:', error);
}
}
async function createAddressTable() {
return new Promise((resolve, reject) => {
db.run('CREATE TABLE IF NOT EXISTS addresses(address TEXT PRIMARY KEY)', (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
async function insertAddress(address) {
return new Promise((resolve, reject) => {
db.run('INSERT OR IGNORE INTO addresses(address) VALUES(?)', [address], function (err) {
if (err) {
reject(err);
} else {
if (this.changes === 0) {
console.log(`Address '${address}' already exists in the database.`);
} else {
console.log(`Address '${address}' inserted into the database.`);
}
resolve();
}
});
});
}
- Cuối cùng check address được tạo random có nằm trong db không
async function doCheck() {
try {
const bip39DataFromDB = await new Promise((resolve, reject) => {
db.all('SELECT * FROM bip39_data', (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows.map(row => row.mnemonic));
}
});
});
let count = 0;
while (true) {
const mnemonic = gen12(bip39DataFromDB);
let wallet = EthHdWallet.fromMnemonic(mnemonic);
let addresses = wallet.generateAddresses(3); // Generate 3 addresses
for (let i = 0; i < 3; i++) {
let address = addresses[i];
db.get('SELECT * FROM addresses WHERE address = ?', [address], (err, row) => {
if (err) {
console.error(err);
return;
}
if (row) {
console.log(address.bgGreen.black);
console.log('Private Key: '.yellow, wallet.privateKey);
} else {
console.log(address.bgGreen.black, 0)
}
});
}
count++;
}
} catch (e) {
doCheck();
}
}
function gen12(words) {
const n = 12;
const shuffledWords = [...words];
for (let i = shuffledWords.length - 1; i > 0; i--) {
const randomIndex = Math.floor(Math.random() * (i + 1));
[shuffledWords[i], shuffledWords[randomIndex]] = [
shuffledWords[randomIndex],
shuffledWords[i],
];
}
return shuffledWords.slice(0, n).join(' ');
}
Kết quả sau vài giờ 🥲
Qua 2 cách thử mặc dù số lượng mnemonic lên tới vài triệu mnemonic nhưng vẫn không thể tìm thấy gì vậy lý do chính ở đây là gì nhỉ: Việc chọn 12 trong 2048 từ sẽ có 2^132 kết hợp, tương đương 5444517870735015415413993718908291383296 , gấp 5444.51787073501 lần 1 triệu tỉ, với số lượng kết hợp lớn như vậy thì tìm được gì đó chắc khác gì mò kim đáy bể đặc biết với siêu máy tính đời nhà tống của mình 😃. Mặc dù biết số lượng lớn kết hợp như vậy nhưng cũng đáng để thử bởi nếu không thử thì sao có bài viết này để kể với mọi người. Biết đâu may mắn tìm được gì đó lại ấm no cả đời thì sao, lúc đó đừng quên quay lại bài viết này và donate cho mình 1 ly cà phê nhé, Good luck!
Lưu ý: Bài viết chỉ nhằm mục đích nghiên cứu, không khuyến khích sử dụng để tìm các địa chỉ ví nếu không được sự cho phép của chủ sở hữu.
All rights reserved