/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/node_sea_bin.cc
484 строки
18 KB
Alex Schwartz
src: fix `--build-sea` default executable path
10 фев 2026, 02:27
Не верифицирован
10 фев 2026, 02:27
30adc7c
Код
Авторство
О чём код?
#include "node_sea.h" #ifdef HAVE_LIEF // Temporarily undefine DEBUG because LIEF uses it as an enum name. #if defined(DEBUG) #define SAVED_DEBUG_VALUE DEBUG #undef DEBUG #include "LIEF/LIEF.hpp" #define DEBUG SAVED_DEBUG_VALUE #undef SAVED_DEBUG_VALUE #else // defined(DEBUG) #include "LIEF/LIEF.hpp" #endif // defined(DEBUG) #endif // HAVE_LIEF #include "debug_utils-inl.h" #include "env-inl.h" #include "node_exit_code.h" #include "simdutf.h" #include "util-inl.h" #include <algorithm> #include <memory> #include <string> #include <string_view> #include <vector> // Split them into two for use later to avoid conflicting with postject. #define SEA_SENTINEL_PREFIX "NODE_SEA_FUSE" #define SEA_SENTINEL_TAIL "fce680ab2cc467b6e072b8b5df1996b2" // The POSTJECT_SENTINEL_FUSE macro is a string of random characters selected by // the Node.js project that is present only once in the entire binary. It is // used by the postject_has_resource() function to efficiently detect if a // resource has been injected. See // https://github.com/nodejs/postject/blob/35343439cac8c488f2596d7c4c1dddfec1fddcae/postject-api.h#L42-L45. #define POSTJECT_SENTINEL_FUSE SEA_SENTINEL_PREFIX "_" SEA_SENTINEL_TAIL #include "postject-api.h" #undef POSTJECT_SENTINEL_FUSE namespace node { namespace sea { // TODO(joyeecheung): use LIEF to locate it directly. std::string_view FindSingleExecutableBlob() { #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) CHECK(IsSingleExecutable()); static const std::string_view result = []() -> std::string_view { size_t size; #ifdef __APPLE__ postject_options options; postject_options_init(&options); options.macho_segment_name = "NODE_SEA"; const char* blob = static_cast<const char*>( postject_find_resource("NODE_SEA_BLOB", &size, &options)); #else const char* blob = static_cast<const char*>( postject_find_resource("NODE_SEA_BLOB", &size, nullptr)); #endif return {blob, size}; }(); per_process::Debug(DebugCategory::SEA, "Found SEA blob %p, size=%zu\n", result.data(), result.size()); return result; #else UNREACHABLE(); #endif // !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) } bool IsSingleExecutable() { return postject_has_resource(); } #ifdef HAVE_LIEF static constexpr const char* kSEAResourceName = "NODE_SEA_BLOB"; static constexpr const char* kELFSectionName = ".note.node.sea"; static constexpr const char* kMachoSegmentName = "NODE_SEA"; enum class InjectResult { kAlreadyExists, kError, kSuccess, kUnknownFormat }; struct InjectOutput { InjectResult result; std::vector<uint8_t> data; // Empty if result != kSuccess std::string error_message; // Populated when result != kSuccess }; template <typename... Args> static void DebugLog(const char* format, Args&&... args) { per_process::Debug(DebugCategory::SEA, "[SEA] "); per_process::Debug(DebugCategory::SEA, format, std::forward<Args>(args)...); } InjectOutput InjectIntoELF(const std::vector<uint8_t>& executable, const std::string& note_name, const std::vector<uint8_t>& data) { DebugLog("Parsing ELF binary for injection...\n"); std::unique_ptr<LIEF::ELF::Binary> binary = LIEF::ELF::Parser::parse(executable); if (!binary) { return {InjectResult::kError, {}, "Failed to parse ELF binary"}; } constexpr uint32_t kNoteType = 0; std::unique_ptr<LIEF::ELF::Note> existing_note; DebugLog("Searching for existing note \'%s\'\n", note_name); for (const auto& n : binary->notes()) { // LIEF can return a length longer than it actually is, so use compare here. if (n.name().compare(0, note_name.size(), note_name) == 0) { DebugLog("Found existing note %s\n", note_name); return {InjectResult::kAlreadyExists, {}, SPrintF("note section %s already exists in the ELF executable", note_name)}; } } DebugLog("No existing note found. Proceeding to add new note.\n"); auto new_note = LIEF::ELF::Note::create(note_name, kNoteType, data, kELFSectionName); if (!new_note) { return {InjectResult::kError, {}, SPrintF("Failed to create new ELF note %s", note_name)}; } binary->add(*new_note); LIEF::ELF::Builder::config_t cfg; cfg.notes = true; // Ensure notes are rebuilt cfg.dynamic_section = true; // Ensure PT_DYNAMIC is rebuilt DebugLog("Building modified ELF binary with new note...\n"); LIEF::ELF::Builder builder(*binary, cfg); builder.build(); if (builder.get_build().empty()) { return {InjectResult::kError, {}, "Failed to build modified ELF binary"}; } return InjectOutput{InjectResult::kSuccess, builder.get_build(), ""}; } InjectOutput InjectIntoMachO(const std::vector<uint8_t>& executable, const std::string& segment_name, const std::string& section_name, const std::vector<uint8_t>& data) { DebugLog("Parsing Mach-O binary for injection...\n"); std::unique_ptr<LIEF::MachO::FatBinary> fat_binary = LIEF::MachO::Parser::parse(executable); if (!fat_binary) { return {InjectResult::kError, {}, "Failed to parse Mach-O binary"}; } // Inject into all Mach-O binaries if there's more than one in a fat binary DebugLog( "Searching for existing section %s/%s\n", segment_name, section_name); for (auto& binary : *fat_binary) { LIEF::MachO::SegmentCommand* segment = binary.get_segment(segment_name); if (!segment) { // Create the segment and mark it read-only LIEF::MachO::SegmentCommand new_segment(segment_name); // Use SegmentCommand::VM_PROTECTIONS enum values (READ) new_segment.max_protection(static_cast<uint32_t>( LIEF::MachO::SegmentCommand::VM_PROTECTIONS::READ)); new_segment.init_protection(static_cast<uint32_t>( LIEF::MachO::SegmentCommand::VM_PROTECTIONS::READ)); LIEF::MachO::Section section(section_name, data); new_segment.add_section(section); binary.add(new_segment); } else { // Check if the section exists LIEF::MachO::Section* existing_section = segment->get_section(section_name); if (existing_section) { // TODO(joyeecheung): support overwrite. return {InjectResult::kAlreadyExists, {}, SPrintF("Segment/section %s/%s already exists in the Mach-O " "executable", segment_name, section_name)}; } LIEF::MachO::Section section(section_name, data); binary.add_section(*segment, section); } // It will need to be signed again anyway, so remove the signature if (binary.has_code_signature()) { DebugLog("Removing existing code signature\n"); if (binary.remove_signature()) { DebugLog("Code signature removed successfully\n"); } else { return {InjectResult::kError, {}, "Failed to remove existing code signature"}; } } } return InjectOutput{InjectResult::kSuccess, fat_binary->raw(), ""}; } InjectOutput InjectIntoPE(const std::vector<uint8_t>& executable, const std::string& resource_name, const std::vector<uint8_t>& data) { DebugLog("Parsing PE binary for injection...\n"); std::unique_ptr<LIEF::PE::Binary> binary = LIEF::PE::Parser::parse(executable); if (!binary) { return {InjectResult::kError, {}, "Failed to parse PE binary"}; } // TODO(postject) - lief.PE.ResourcesManager doesn't support RCDATA it seems, // add support so this is simpler? if (!binary->has_resources()) { // TODO(postject) - Handle this edge case by creating the resource tree return { InjectResult::kError, {}, "PE binary has no resources, cannot inject"}; } LIEF::PE::ResourceNode* resources = binary->resources(); LIEF::PE::ResourceNode* rcdata_node = nullptr; LIEF::PE::ResourceNode* id_node = nullptr; // First level => Type (ResourceDirectory node) DebugLog("Locating/creating RCDATA resource node\n"); constexpr uint32_t RCDATA_ID = static_cast<uint32_t>(LIEF::PE::ResourcesManager::TYPE::RCDATA); auto rcdata_node_iter = std::find_if(std::begin(resources->childs()), std::end(resources->childs()), [](const LIEF::PE::ResourceNode& node) { return node.id() == RCDATA_ID; }); if (rcdata_node_iter != std::end(resources->childs())) { DebugLog("Found existing RCDATA resource node\n"); rcdata_node = &*rcdata_node_iter; } else { DebugLog("Creating new RCDATA resource node\n"); LIEF::PE::ResourceDirectory new_rcdata_node; new_rcdata_node.id(RCDATA_ID); rcdata_node = &resources->add_child(new_rcdata_node); } // Second level => ID (ResourceDirectory node) DebugLog("Locating/creating ID resource node for %s\n", resource_name); DCHECK(simdutf::validate_ascii(resource_name.data(), resource_name.size())); std::u16string resource_name_u16(resource_name.begin(), resource_name.end()); auto id_node_iter = std::find_if(std::begin(rcdata_node->childs()), std::end(rcdata_node->childs()), [resource_name_u16](const LIEF::PE::ResourceNode& node) { return node.name() == resource_name_u16; }); if (id_node_iter != std::end(rcdata_node->childs())) { DebugLog("Found existing ID resource node for %s\n", resource_name); id_node = &*id_node_iter; } else { // TODO(postject) - This isn't documented, but if this isn't set then // LIEF won't save the name. Seems like LIEF should be able // to automatically handle this if you've set the node's name DebugLog("Creating new ID resource node for %s\n", resource_name); LIEF::PE::ResourceDirectory new_id_node; new_id_node.name(resource_name); new_id_node.id(0x80000000); id_node = &rcdata_node->add_child(new_id_node); } // Third level => Lang (ResourceData node) DebugLog("Locating existing language resource node for %s\n", resource_name); if (id_node->childs() != std::end(id_node->childs())) { DebugLog("Found existing language resource node for %s\n", resource_name); return {InjectResult::kAlreadyExists, {}, SPrintF("Resource %s already exists in the PE executable", resource_name)}; } LIEF::PE::ResourceData lang_node(data); id_node->add_child(lang_node); DebugLog("Rebuilding PE resources with new data for %s\n", resource_name); // Write out the binary, only modifying the resources LIEF::PE::Builder::config_t cfg; cfg.resources = true; cfg.rsrc_section = ".rsrc"; // ensure section name LIEF::PE::Builder builder(*binary, cfg); if (!builder.build()) { return {InjectResult::kError, {}, "Failed to build modified PE binary"}; } return InjectOutput{InjectResult::kSuccess, builder.get_build(), ""}; } void MarkSentinel(InjectOutput* output, const std::string& sentinel_fuse) { if (output == nullptr || output->result != InjectResult::kSuccess) return; std::string_view fuse(sentinel_fuse); std::string_view data_view(reinterpret_cast<char*>(output->data.data()), output->data.size()); size_t first_pos = data_view.find(fuse); DebugLog("Searching for fuse: %s\n", sentinel_fuse); if (first_pos == std::string::npos) { output->result = InjectResult::kError; output->error_message = SPrintF("sentinel %s not found", sentinel_fuse); return; } size_t last_pos = data_view.rfind(fuse); if (first_pos != last_pos) { output->result = InjectResult::kError; output->error_message = SPrintF("found more than one occurrence of sentinel %s", sentinel_fuse); return; } size_t colon_pos = first_pos + fuse.size(); if (colon_pos >= data_view.size() || data_view[colon_pos] != ':') { output->result = InjectResult::kError; output->error_message = SPrintF("missing ':' after sentinel %s", sentinel_fuse); return; } size_t idx = colon_pos + 1; // Expecting ':0' or ':1' after the fuse if (idx >= data_view.size()) { output->result = InjectResult::kError; output->error_message = "Sentinel index out of range"; return; } DebugLog("Found fuse: %s\n", data_view.substr(first_pos, fuse.size() + 2)); if (data_view[idx] == '0') { DebugLog("Marking sentinel as 1\n"); output->data.data()[idx] = '1'; } else if (data_view[idx] == '1') { output->result = InjectResult::kAlreadyExists; output->error_message = "Sentinel is already marked"; return; } else { output->result = InjectResult::kError; output->error_message = SPrintF("Sentinel has invalid value %d", static_cast<int>(data_view[idx])); return; } DebugLog("Processed fuse: %s\n", data_view.substr(first_pos, fuse.size() + 2)); return; } InjectOutput InjectResource(const std::vector<uint8_t>& exe, const std::string& resource_name, const std::vector<uint8_t>& res, const std::string& macho_segment_name) { if (LIEF::ELF::is_elf(exe)) { return InjectIntoELF(exe, resource_name, res); } else if (LIEF::MachO::is_macho(exe)) { std::string sec = resource_name; if (!(sec.rfind("__", 0) == 0)) sec = "__" + sec; return InjectIntoMachO(exe, macho_segment_name, sec, res); } else if (LIEF::PE::is_pe(exe)) { std::string upper_name = resource_name; // Convert resource name to uppercase as PE resource names are // case-insensitive. std::transform(upper_name.begin(), upper_name.end(), upper_name.begin(), [](unsigned char c) { return std::toupper(c); }); return InjectIntoPE(exe, upper_name, res); } return {InjectResult::kUnknownFormat, {}, "Executable must be a supported format: ELF, PE, or Mach-O"}; } ExitCode BuildSingleExecutable(const std::string& sea_config_path, const std::vector<std::string>& args, const std::vector<std::string>& exec_args) { std::optional<SeaConfig> opt_config = ParseSingleExecutableConfig(sea_config_path); if (!opt_config.has_value()) { return ExitCode::kGenericUserError; } SeaConfig config = opt_config.value(); if (config.executable_path.empty()) { config.executable_path = Environment::GetExecPath(args); } // Get file permissions from source executable to copy over later. uv_fs_t req; int r = uv_fs_stat(nullptr, &req, config.executable_path.c_str(), nullptr); if (r != 0) { FPrintF(stderr, "Error: Couldn't stat executable %s: %s\n", config.executable_path, uv_strerror(r)); uv_fs_req_cleanup(&req); return ExitCode::kGenericUserError; } int src_mode = static_cast<int>(req.statbuf.st_mode); uv_fs_req_cleanup(&req); std::vector<uint8_t> exe_data; r = ReadFileSync(config.executable_path.c_str(), &exe_data); if (r != 0) { FPrintF(stderr, "Error: Couldn't read executable %s: %s\n", config.executable_path, uv_strerror(r)); return ExitCode::kGenericUserError; } std::vector<char> sea_blob; ExitCode code = GenerateSingleExecutableBlob(&sea_blob, config, args, exec_args); if (code != ExitCode::kNoFailure) { return code; } // TODO(joyeecheung): refactor serializer implementation and avoid copying std::vector<uint8_t> sea_blob_u8(sea_blob.begin(), sea_blob.end()); // For backward compatibility with postject, we construct the sentinel fuse // at runtime instead using a constant. std::string fuse = std::string(SEA_SENTINEL_PREFIX) + "_" + SEA_SENTINEL_TAIL; InjectOutput out = InjectResource( exe_data, kSEAResourceName, sea_blob_u8, kMachoSegmentName); if (out.result == InjectResult::kSuccess) { MarkSentinel(&out, fuse); } if (out.result != InjectResult::kSuccess) { if (!out.error_message.empty()) { FPrintF(stderr, "Error: %s\n", out.error_message); } return ExitCode::kGenericUserError; } uv_buf_t buf = uv_buf_init(reinterpret_cast<char*>(out.data.data()), static_cast<size_t>(out.data.size())); r = WriteFileSync(config.output_path.c_str(), buf); if (r != 0) { FPrintF(stderr, "Error: Couldn't write output executable: %s: %s\n", config.output_path, uv_strerror(r)); return ExitCode::kGenericUserError; } // Copy file permissions (including execute bit) from source executable r = uv_fs_chmod(nullptr, &req, config.output_path.c_str(), src_mode, nullptr); uv_fs_req_cleanup(&req); if (r != 0) { FPrintF(stderr, "Warning: Couldn't set permissions %d on %s: %s\n", src_mode, config.output_path, uv_strerror(r)); } FPrintF(stdout, "Generated single executable %s + %s -> %s\n", config.executable_path, sea_config_path, config.output_path); return ExitCode::kNoFailure; } #else ExitCode BuildSingleExecutable(const std::string& sea_config_path, const std::vector<std::string>& args, const std::vector<std::string>& exec_args) { FPrintF( stderr, "Error: Node.js must be built with the LIEF library to support built-in" " single executable applications.\n"); return ExitCode::kGenericUserError; } #endif // HAVE_LIEF } // namespace sea } // namespace node