Размножаемся
Игра дает нам программу NUKE.EXE
, которая взламывает компьютер и получает права администратора. Программа поможет вирусу захватывать компьютеры.
Иногда NUKE.EXE
требует, чтобы компьютер-жертва открыл сетевые порты. Позже научимся взламывать порты, а пока ограничимся жертвами, что поддаются NUKE.EXE
и без открытых портов.
Взлом компьютера требует навыков — хакерского уровня. NUKE.EXE
не взломает компьютер, если уровень ниже требуемого. Вы повышаете уровень, когда взламываете компьютеры и учитесь в университете.
Напишем скрипт, который заражает соседние компьютеры:
// 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; }
Функция
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 ... */
Компьютер способен запустить дополнительные потоки, когда память свободна. Функция 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); } }
Повелеваем и властвуем
Мы захватили компьютеры сети, но пока не способны ими управлять. Научим вирус получать команды по сети. Предлагаю два способа:
-
Вирус подключается к управляющему серверу и получает команды. Такая сеть вирусов умрет, когда умрет управляющий сервер.
-
Вирус получает команды от соседей. Владелец сети отдает команды любому компьютеру и команды оказываются у остальных. Такую сеть вирусов победить труднее — придется вылечить каждый компьютер.
Первый способ проще — каждый компьютер знает адрес сервера, подключается и выполняет команды.
//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()); }
Пусть вирус перезапишет файл команд, только когда получит следующую версию. Функция 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
подорожала на 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
Играйте с пользой!
ссылка на оригинал статьи https://habr.com/ru/articles/866590/
Добавить комментарий