Игрушечный ботнет на JavaScript под BitBurner

от автора

Размножаемся

Игра дает нам программу NUKE.EXE, которая взламывает компьютер и получает права администратора. Программа поможет вирусу захватывать компьютеры.

Иногда NUKE.EXE требует, чтобы компьютер-жертва открыл сетевые порты. Позже научимся взламывать порты, а пока ограничимся жертвами, что поддаются NUKE.EXE и без открытых портов.

Взлом компьютера требует навыков — хакерского уровня. NUKE.EXE не взломает компьютер, если уровень ниже требуемого. Вы повышаете уровень, когда взламываете компьютеры и учитесь в университете.

Sector-12

Sector-12

Напишем скрипт, который заражает соседние компьютеры:

// worm-01.js /** @param {NS} ns */ export async function main(ns) {   while (true) {     let victims = ns.scan();     for (let i in victims) {       if (!isInfected(ns, victims[i]))         infect(ns, victims[i]);     }     await ns.sleep(15000);   } }  /**   * @param {NS} ns  * @param {string} host */ function isInfected(ns, host) {   return ns.hasRootAccess(host)     && ns.isRunning(ns.getScriptName(), host); }  /**   * @param {NS} ns  * @param {string} host */ function infect(ns, host) {   ns.print("infect ", host);   grantRootAccess(ns, host);   if (ns.hasRootAccess(host)) {     ns.scp(ns.getScriptName(), host);     ns.exec(ns.getScriptName(), host);   } }  /**   * @param {NS} ns  * @param {string} host */ function grantRootAccess(ns, host) {   if (ns.hasRootAccess(host)) return true;    if (ns.getServerRequiredHackingLevel(host) <= ns.getHackingLevel()) {     const s = ns.getServer(host);     if (s.numOpenPortsRequired <= s.openPortCount)       ns.nuke(host);     else       ns.printf("Cannot grant root access on '%s': %d open ports required, %d opened",         host, s.numOpenPortsRequired, s.openPortCount);      return ns.hasRootAccess(host);   }    return false; } 
Червь 1 размножается

Червь 1 размножается

Функция ns.sleep работает асинхронно — возвращает управление программе сразу, но функция еще не завершила работу. Оператор await ждет, пока функция завершится.

Асинхронные функции помогают программам и устройствам ввода-вывода работать параллельно. Примеры:

  • Программа отправляет сообщение по сети и выполняет другой код, пока ждет ответа

  • Драйвер просит диск записать блоки файла и выполняет другой код, пока диск выполняет просьбу. xv6: Прерывания и драйверы устройств

Собираем дань

Вирус бесполезен, если только размножается. Научим вирус грабить соседей.

//robber.js /** @param {NS} ns */ export async function main(ns) {   while (true) {     let victims = ns.scan();     for (let i in victims) {       const host = victims[i];       if ("home" == host) {          // игра требует выполнять await на каждой итерации цикла, иначе зависнет         await ns.sleep(1);         continue;        }        if (ns.hasRootAccess(host)) {         if (ns.getServerMinSecurityLevel(host) < ns.getServerSecurityLevel(host)) {           await ns.weaken(host);         } else if (ns.getServerMoneyAvailable(host) < ns.getServerMaxMoney(host)) {           await ns.grow(host);         } else {           await ns.hack(host);         }       }     }   } } 
//worm-02.js const SCRIPT_ROBBER = "robber.js";  /**   * @param {NS} ns  * @param {string} host */ function infect(ns, host) {   grantRootAccess(ns, host);   if (ns.hasRootAccess(host)) {     ns.scp(ns.getScriptName(), host);     ns.exec(ns.getScriptName(), host);      ns.scp(SCRIPT_ROBBER, host);     ns.exec(SCRIPT_ROBBER, host);   } }  /* ... остальной код из worm-01.js ... */ 
Червь 2 запускает грабителя

Червь 2 запускает грабителя

Компьютер способен запустить дополнительные потоки, когда память свободна. Функция execScriptIfEnoughRam выполняет как можно больше потоков скрипта.

//worm-03.js /**   * @param {NS} ns  * @param {string} host */ function execScriptIfEnoughRam(ns, scriptFileName, host, maxThreads) {   const threads = Math.min(countPossibleThreads(ns, scriptFileName, host), maxThreads);   if (0 < threads)      ns.exec(scriptFileName, host, threads); }  /**   * @param {NS} ns  * @param {string} host */ function countPossibleThreads(ns, scriptFileName, host) {   const maxRam = ns.getServerMaxRam(host);   const freeRam = maxRam - ns.getServerUsedRam(host);   const ramCost = ns.getScriptRam(scriptFileName, host);   return Math.floor(freeRam / ramCost); }  /**   * @param {NS} ns  * @param {string} host */ function infect(ns, host) {   grantRootAccess(ns, host);   if (ns.hasRootAccess(host)) {     ns.scp(ns.getScriptName(), host);     execScriptIfEnoughRam(ns, ns.getScriptName(), host, 1);      ns.scp(SCRIPT_ROBBER, host);     execScriptIfEnoughRam(ns, SCRIPT_ROBBER, host, 999);   } } 
Червь 3 запускает трех грабителей

Червь 3 запускает трех грабителей

Повелеваем и властвуем

Мы захватили компьютеры сети, но пока не способны ими управлять. Научим вирус получать команды по сети. Предлагаю два способа:

  • Вирус подключается к управляющему серверу и получает команды. Такая сеть вирусов умрет, когда умрет управляющий сервер.

  • Вирус получает команды от соседей. Владелец сети отдает команды любому компьютеру и команды оказываются у остальных. Такую сеть вирусов победить труднее — придется вылечить каждый компьютер.

Первый способ проще — каждый компьютер знает адрес сервера, подключается и выполняет команды.

//worm-04.js const COMMANDS_FILE = "todo.txt";  /**   * @param {NS} ns  * @param {string} host */ function downloadCommandFile(ns, host) {   return ns.scp(COMMANDS_FILE, ns.getHostname(), host); }  /** @param {NS} ns */ async function processCommandFile(ns) {   const lines = ns.read(COMMANDS_FILE).split(/\n|\r\n/);   for (let i in lines) {     const words = lines[i].split(' ');     if (0 < words.length) {       const command = words.shift();       await processCommand(ns, command, words);     }   } }  /** @param {NS} ns */ export async function main(ns) {   while (true) {     let victims = ns.scan();     for (let i in victims) {       if (!isInfected(ns, victims[i]))         infect(ns, victims[i]);     }     downloadCommandFile(ns, "home");     await processCommandFile(ns);      await ns.sleep(15000);   } } 
Червь ожирел

Червь ожирел

Скрипт worm-04.js перестал влезать в память. Функция getServer жрет памяти больше остальных — избавимся от нее.

/**   * @param {NS} ns  * @param {string} host */ function grantRootAccess(ns, host) {   if (ns.hasRootAccess(host)) return true;    try {     ns.nuke(host);   } catch (e) {     ns.print(`Cannot grant root access on '${host}': ${e}`);   }    return ns.hasRootAccess(host); } 
Червь облегчился

Червь облегчился

Теперь научим вирус получать команды от соседей.

//worm-05.js /** @param {NS} ns */ export async function main(ns) {   while (true) {     let victims = ns.scan();     for (let i in victims) {       const host = victims[i];       if (!isInfected(ns, host))         infect(ns, host);        downloadCommandFile(ns, host);     }     await processCommandFile(ns);     await ns.sleep(15000);   } } 

Учим крестьян знать барина в лицо

Прежде вирус знал — управляющий сервер хранит последние команды, что отдал владелец. Теперь вирус не знает, получил ли сосед последние команды или еще не успел. Пометим файлы команд номерами, чтобы отличать старые и новые.

//worm-05.js /**   * @param {NS} ns  * @param {string} fileName */ function getCommandsFileVersion(ns, fileName) {   return parseInt(ns.read(fileName).split(/\n|\r\n/).shift()); } 
todo.txt

todo.txt

Пусть вирус перезапишет файл команд, только когда получит следующую версию. Функция scp() перезаписывает файлы всегда, поэтому напишем функцию downloadFile, что сохраняет файл под другим именем.

//worm-05.js /**   * @param {NS} ns  * @param {string} host */ function downloadCommandFile(ns, host) {   const tempFile = getTemporaryFileName();   downloadFile(ns, host, COMMANDS_FILE, tempFile);   if (getCommandsFileVersion(ns, COMMANDS_FILE) < getCommandsFileVersion(ns, tempFile))     ns.mv(ns.getHostname(), tempFile, COMMANDS_FILE); }  function getTemporaryFileName() {   const now = new Date().getTime();   return `${TMP_DIR}/${now}.txt`; }  /**   * @param {NS} ns  * @param {string} sourceFileName  * @param {string} destinationFileName */ function downloadFile(ns, remoteHost, sourceFileName, destinationFileName) {   const localHost = ns.getHostname();   const backup = backupFile(ns, sourceFileName);   ns.scp(sourceFileName, localHost, remoteHost);   ns.mv(localHost, sourceFileName, destinationFileName);   if (backup) ns.mv(localHost, backup, sourceFileName); }  /**   * @param {NS} ns  * @param {string} fileName */ function backupFile(ns, fileName) {   const backupFileName = `${fileName}.backup.txt`;   ns.write(backupFileName, ns.read(fileName), "w");   return backupFileName; } 

Подпишем командный файл, чтобы вирус выполнял только команды владельца. Скрипт sign.js подписывает файл, а verify.js проверяет подпись. Скрипт generateKeys.js создает пару ключей — для подписи и проверки.

//sign.js /** @param {NS} ns */ export async function main(ns) {   if (ns.args.length < 3) {     return usage(ns);   }    const keyFileName = ns.args[0];   const dataFileName = ns.args[1];   const outputFileName = ns.args[2];   if (!assertFileExists(ns, keyFileName)) return;   if (!assertFileExists(ns, dataFileName)) return;    const pemEncodedKey = ns.read(keyFileName);   const key = await importPrivateKey(pemEncodedKey);   const data = new TextEncoder().encode(ns.read(dataFileName));   let signature = await crypto.subtle.sign(     {       name: "ECDSA",       hash: { name: "SHA-384" },     },     key,     data,   );   let output = btoa(ab2str(signature));   ns.write(outputFileName, output, "w");   ns.tprintf("%d bytes written to '%s'", output.length, outputFileName); }  //FIXME Protect private key with a password function importPrivateKey(pem) {   // fetch the part of the PEM string between header and footer   const pemHeader = "-----BEGIN PRIVATE KEY-----";   const pemFooter = "-----END PRIVATE KEY-----";   const pemContents = pem.substring(     pemHeader.length,     pem.length - pemFooter.length - 1,   );   // base64 decode the string to get the binary data   const binaryDerString = atob(pemContents);   // convert from a binary string to an ArrayBuffer   const binaryDer = str2ab(binaryDerString);    return crypto.subtle.importKey(     "pkcs8",     binaryDer,     {       name: "ECDSA",       namedCurve: "P-384",     },     true,     ["sign"]   ); }  /* Convert an ArrayBuffer into a string from https://developer.chrome.com/blog/how-to-convert-arraybuffer-to-and-from-string/ */ function ab2str(buf) {   return String.fromCharCode.apply(null, new Uint8Array(buf)); }  /* Convert a string into an ArrayBuffer from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String */ function str2ab(str) {   const buf = new ArrayBuffer(str.length);   const bufView = new Uint8Array(buf);   for (let i = 0, strLen = str.length; i < strLen; i++) {     bufView[i] = str.charCodeAt(i);   }   return buf; }  /** @param {NS} ns */ function usage(ns) {   ns.tprintf("Usage: $0 <private-key-file> <file-to-sign> <output-file>"); }  /**   * @param {NS} ns  * @param {string} fileName */ function assertFileExists(ns, fileName) {   const exists = ns.fileExists(fileName);   if (!exists)     ns.tprint("ERROR: File '", fileName, "' does not exist");    return exists; } 
//verify.js /** @param {NS} ns */ export async function main(ns) {   if (ns.args.length < 3) return usage(ns);    const publicKeyFileName = ns.args[0];   const dataFileName = ns.args[1];   const signatureFileName = ns.args[2];   if (!assertFileExists(ns, publicKeyFileName)) return;   if (!assertFileExists(ns, dataFileName)) return;   if (!assertFileExists(ns, signatureFileName)) return;    const key = await importPublicKey(ns.read(publicKeyFileName));   const message = new TextEncoder().encode(ns.read(dataFileName));   const signature = str2ab(atob(ns.read(signatureFileName)));   let result = await crypto.subtle.verify(     {       name: "ECDSA",       hash: { name: "SHA-384" },     },     key,     signature,     message,   );   ns.tprint(result ? "OK" : "INVALID"); }  /**  * @param {string} pem */ function importPublicKey(pem) {   // fetch the part of the PEM string between header and footer   const pemHeader = "-----BEGIN PUBLIC KEY-----";   const pemFooter = "-----END PUBLIC KEY-----";   const pemContents = pem.substring(     pemHeader.length,     pem.length - pemFooter.length - 1,   );   // base64 decode the string to get the binary data   const binaryDerString = atob(pemContents);   // convert from a binary string to an ArrayBuffer   const binaryDer = str2ab(binaryDerString);    return crypto.subtle.importKey(     "spki",     binaryDer,     {       name: "ECDSA",       namedCurve: "P-384",     },     true,     ["verify"]   ); }  /** @param {NS} ns */ function usage(ns) {   ns.tprintf("Usage: $0 <public-key-file> <data-file> <signature-file>"); }  
//generateKeys.js /** @param {NS} ns */ export async function main(ns) {   if (ns.args.length < 2) {     return usage(ns);   }    const privateKeyFileName = ns.args[0];   const publicKeyFileName = ns.args[1];   const { publicKey, privateKey } = await crypto.subtle.generateKey({     name: "ECDSA",     namedCurve: "P-384",   },     true,     ["sign", "verify"],   );   await savePrivateKeyToFile(ns, privateKey, privateKeyFileName);   await savePublicKeyToFile(ns, publicKey, publicKeyFileName); }  /**   * @param {NS} ns  * @param {CryptoKey} key  * @param {string} fileName */ async function savePrivateKeyToFile(ns, key, fileName) {   const exported = await crypto.subtle.exportKey("pkcs8", key);   const exportedAsString = ab2str(exported);   const exportedAsBase64 = btoa(exportedAsString);   const pemExported = `-----BEGIN PRIVATE KEY-----\n${exportedAsBase64}\n-----END PRIVATE KEY-----`;   ns.write(fileName, pemExported, "w"); }  /**   * @param {NS} ns  * @param {CryptoKey} key  * @param {string} fileName */ async function savePublicKeyToFile(ns, key, fileName) {   const exported = await crypto.subtle.exportKey("spki", key);   const exportedAsString = ab2str(exported);   const exportedAsBase64 = btoa(exportedAsString);   const pemExported = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`;   ns.write(fileName, pemExported, "w"); } 

Игра разрешает писать только .txt и .js файлы, поэтому файл подписи назовем todo.txt.sig.txt.

run generateKeys.js keys/sign.txt keys/verify.txt run sign.js keys/sign.txt todo.txt todo.txt.sig.txt run verify.js keys/verify.txt todo.txt todo.txt.sig.txt 

Пусть вирус перезаписывает файл команд, только если получил новую версию и подпись верна.

//worm-06.js /**   * @param {NS} ns  * @param {string} host */ async function downloadCommandFile(ns, host) {   const tempFile = getTemporaryFileName();   downloadFile(ns, host, COMMANDS_FILE, tempFile);   const signatureFileName = getSignatureFileName(COMMANDS_FILE);   const tempSignatureFile = getTemporaryFileName();   downloadFile(ns, host, signatureFileName, tempSignatureFile);   const isSignatureValid = await verifyFileSignature(ns, PUBLIC_KEY_FILE, tempFile, tempSignatureFile);   if (isSignatureValid     && getCommandsFileVersion(ns, COMMANDS_FILE) < getCommandsFileVersion(ns, tempFile)) {     ns.mv(ns.getHostname(), tempFile, COMMANDS_FILE);     ns.mv(ns.getHostname(), tempSignatureFile, getSignatureFileName(COMMANDS_FILE));   } }  /**   * @param {NS} ns   * @param {string} publicKeyFileName   * @param {string} dataFileName   * @param {string} signatureFileName */ async function verifyFileSignature(ns, publicKeyFileName, dataFileName, signatureFileName) {   const pemEncodedKey = ns.read(publicKeyFileName);   const key = await importPublicKey(pemEncodedKey);   const data = new TextEncoder().encode(ns.read(dataFileName));   const signature = str2ab(atob(ns.read(signatureFileName)));   return await crypto.subtle.verify({ name: "ECDSA", hash: { name: "SHA-384" } },     key, signature, data); } 

Пусть вирус проверит подпись файла, прежде чем выполнять команды.

//worm-06.js /**   * @param {NS} ns  * @param {string} fileName */ async function processCommandFile(ns) {   const isSignatureValid = await verifyFileSignature(     ns, PUBLIC_KEY_FILE, COMMANDS_FILE, getSignatureFileName(COMMANDS_FILE));   if (!isSignatureValid) {     ns.print("processCommandFile: invalid file signature");     return;   }    const lines = ns.read(COMMANDS_FILE).split(/\n|\r\n/).splice(1);  // skip file version   for (let i in lines) {     const words = lines[i].split(' ');     if (0 < words.length) {       const command = words.shift();       await processCommand(ns, command, words);     }   } } 

Функция crypto.subtle.verify — асинхронная, поэтому асинхронными стали и функции processCommandFile, downloadCommandFile, verifyFileSignature.

Команды

Вирус выполняет такие команды:

  • run <script-name> <max-threads> запускает скрипт

  • kill <script-name> останавливает скрипт

  • sleep <milliseconds> спит

  • share делится ресурсами жертвы с другими хакерами. Пригодится, если решите пройти игру по сюжету.

/**  *   * @param {NS} ns   * @param {string} command   * @param {string[]} args   */ async function processCommand(ns, command, args) {   const now = new Date();   const timeStr = `[${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}] `;   ns.print(timeStr, `processCommand: ${command}`);   if ("run" == command) {     commandRun(ns, args);   } else if ("kill" == command) {     commandKill(ns, args);   } else if ("share" == command) {     await ns.share();   } else if ("sleep" == command) {     const timeout = (0 < args.length) ? parseInt(args[0]) : null;     if (timeout)       await ns.sleep(timeout);   } }  /**   * @param {NS} ns  * @param {string[]} args */ function commandRun(ns, args) {   if (0 < args.length) {     const scriptName = args[0];     let threads = (1 < args.length) ? parseInt(args[1]) : null;     if (!threads) threads = 1;      if (!ns.isRunning(scriptName, ns.getHostname()))       execScriptIfEnoughRam(ns, scriptName, ns.getHostname(), threads);   } }  /**   * @param {NS} ns  * @param {string[]} args */ function commandKill(ns, args) {   if (0 < args.length)     ns.scriptKill(args[0], ns.getHostname()); } 

Вызов ns.share() отнимает у вируса 2.40GB памяти — вынесем ns.share() в отдельный скрипт share.js. Вирус менее заметен, когда жрет меньше памяти. Поэтому мы вынесли ns.grow(), ns.weaken() и ns.hack() в robber.js.

SCRIPT_SHARE = "share.js";  /**   * @param {NS} ns  * @param {string} host */ function infect(ns, host) {   grantRootAccess(ns, host);   if (ns.hasRootAccess(host)) {     ns.scp(ns.getScriptName(), host);     execScriptIfEnoughRam(ns, ns.getScriptName(), host, 1);      ns.scp(COMMANDS_FILE, host);     ns.scp(getSignatureFileName(COMMANDS_FILE), host);     ns.scp(PUBLIC_KEY_FILE, host);     ns.scp(SCRIPT_ROBBER, host);     ns.scp(SCRIPT_SHARE, host);   } }  /**  *   * @param {NS} ns   * @param {string} command   * @param {string[]} args   */ async function processCommand(ns, command, args) {   const now = new Date();   const timeStr = `[${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}] `;   ns.print(timeStr, `processCommand: ${command}`);   if ("run" == command) {     commandRun(ns, args);   } else if ("kill" == command) {     commandKill(ns, args);   } else if ("share" == command) {     ns.run(SCRIPT_SHARE);   } else if ("sleep" == command) {     const timeout = (0 < args.length) ? parseInt(args[0]) : null;     if (timeout)       await ns.sleep(timeout);   } } 
share.js

share.js

Команда share подорожала на 1.60GB, но вирус похудел. Мы экономим память, если share вызывают редко.

Заключение

BitBurner — для тех, кто любит программировать. Игра не ограничивает фантазию игрока — умеет все, что умеет JavaScript.

Забавно, что вызовы ns требуют памяти, но другие функции JavaScript — шифрования, кодирования, даты и времени — скрипт вызывает на халяву. Игра оштрафует скрипт на 25.00GB только когда он обратится к window:

//sign.js export async function main(ns) {   //...   let signature = await window.crypto.subtle.sign(    //... } 
[home /]> mem sign.js  This script requires 26.70GB of RAM to run for 1 thread(s)  25.00GB | window (dom)   1.60GB | baseCost (misc)   0.10GB | fileExists (fn) 
//sign.js export async function main(ns) {   //...   let signature = await crypto.subtle.sign(    //... } 
This script requires 1.70GB of RAM to run for 1 thread(s)   1.60GB | baseCost (misc)   0.10GB | fileExists (fn) 

Исходный код BitBurner на GitHub

Файлы к статье

BitBurner в Steam

Играйте с пользой!


ссылка на оригинал статьи https://habr.com/ru/articles/866590/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *