{"id":27942566,"url":"https://github.com/dariushoule/node-sea-scallop","last_synced_at":"2026-04-29T00:31:51.347Z","repository":{"id":289010603,"uuid":"969785253","full_name":"dariushoule/node-sea-scallop","owner":"dariushoule","description":"Scallop is a multi-tool for unpacking, repacking, and script stomping nodejs single executable applications (SEA)s.","archived":false,"fork":false,"pushed_at":"2025-04-22T17:35:59.000Z","size":32811,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-07T11:57:06.538Z","etag":null,"topics":["malware-analysis","nodejs","onefile","redteam","reverse-engineering","unpacking"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dariushoule.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-04-20T23:22:47.000Z","updated_at":"2025-04-23T18:01:33.000Z","dependencies_parsed_at":"2025-04-21T03:33:04.878Z","dependency_job_id":"63b1a2f1-2b7d-49d0-b157-7c17fb56b0ab","html_url":"https://github.com/dariushoule/node-sea-scallop","commit_stats":null,"previous_names":["dariushoule/node-sea-scallop"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dariushoule%2Fnode-sea-scallop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dariushoule%2Fnode-sea-scallop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dariushoule%2Fnode-sea-scallop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dariushoule%2Fnode-sea-scallop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dariushoule","download_url":"https://codeload.github.com/dariushoule/node-sea-scallop/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252873986,"owners_count":21817711,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["malware-analysis","nodejs","onefile","redteam","reverse-engineering","unpacking"],"created_at":"2025-05-07T11:57:10.116Z","updated_at":"2026-04-29T00:31:46.328Z","avatar_url":"https://github.com/dariushoule.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Scallop: The Node SEA Swiss Army Knife\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/dariushoule/node-sea-scallop/main/scallop.png\" alt=\"scallop\"\u003e\u003c/p\u003e\n\n**Scallop is a multi-tool for unpacking, repacking, and script stomping nodejs single executable applications (SEA)s.**\n\nThe project serves source code recovery, malware analysis, red-teaming, and SEA internals exploration.\n\n**Compatibility Matrix**\n\n| OS      | Node Version | Unpack | Repack | Stomp | Repack Asset |\n|---------|--------------|--------|--------|-------|--------------|\n| Windows |           23 |      ✅|    ✅ |    ✅ |          ✅ |\n| Windows |           22 |      ✅|    ✅ |    ✅ |          ✅ |\n| Linux   |           23 |      ✅|    ✅ |    ✅ |          ✅ |\n| Linux   |           22 |      ✅|    ✅ |    ✅ |          ✅ |\n| MacOS¹  |           23 |      ✅|    ✅ |    ✅ |          ✅ |\n| MacOS¹  |           22 |      ✅|    ✅ |    ✅ |          ✅ |\n\n¹ On MacOS, repacked binaries will not execute unless they are re-codesigned or manually excluded from codesigning.\n\n## Installation\n\n```bash\npip install node-sea-scallop\n\nscallop --help\n```\n\n## Modes of Operation\n### Unpack\n\nUnpack extracts:\n1. 🤖 The main javascript bundle from the binary's embedded SEA blob\n2. 💾 The main code cache (if it exists) from the binary's embedded SEA blob\n3. 🖼️ The embedded assets (if they exist) from the binary's embedded SEA blob\n4. 🥩 The raw SEA blob\n\n```bash\nscallop unpack \u003ctarget_sea_binary\u003e\n```\n\nImportant Notes:\n1. Output is created in the same directory as `target_sea_binary` under `\u003ctarget_sea_binary\u003e_unpacked/`\n\n\n### Repack Main Code Resource (without stomping)\n\nRepack replaces the main javascript bundle (or snapshot) with a file of your choosing.\n\n```bash\nscallop repack \u003ctarget_sea_binary\u003e \u003creplacement_js_file_or_v8_snapshot\u003e\n```\n\nImportant Notes:\n1. Content is repacked in-place.\n2. The Code cache is cleared by default when using this configuration.\n3. *If your SEA is code signed, repacking will make the signature invalid. You'll need to be able to resign the binary to make it valid. If your SEA is not codesigned, everything will work as expected.*\n\n\n### Repack Main Code Resource (script stomped)\n\nRepack replaces the main javascript bundle (or snapshot) with a file of your choosing. The script is stomped by the code cache. \n\n```bash\nscallop repack \u003ctarget_sea_binary\u003e \u003creplacement_js_file_or_v8_snaphot\u003e --stomp\n```\n\n🔔 It should be noted that the script cannot be replaced with arbitrary content, it still must structurally match the original content to avoid crashing. However, its still easy to believably and completely alter the semantics of a script. A Contrived Example might look like:\n\n**Example (Note Structural Similarity)**\n\n*Original Script*\n```javascript\n// This script executes an evil PowerShell command using a child process.\n\nconst child_process_s = [0x63,0x68,0x69,0x6c,0x64,0x5f,0x70,0x72,0x6f,0x63,0x65,0x73,0x73].map((v) =\u003e String.fromCharCode(v)).join('');\nconst { exec } = require(child_process_s);\n\nconst evil_powershell_command = [0x70,0x6f,0x77,0x65,0x72,0x73,0x68,0x65,0x6c,0x6c,0x20,0x65,0x63,0x68,0x6f,0x20,0x22,0x65,0x76,0x69,0x6c,0x20,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64,0x22].map((v) =\u003e String.fromCharCode(v)).join('');\nexec(evil_powershell_command, (_, stdout) =\u003e {\n    console.log(stdout);\n})\n```\n\n*Stomped Script*\n```javascript\n// This script synchronizes with a remote NTP server and prints the time.\n\nconst ntp_ipv6_addres = \"f609:ff62:ae3d:7ce2:0bb9:807b:3043:65c6:56a6:56db:6a89:9665:9876\".ip6((o) =\u003e String.fromCharCode(o)).inet('');\nconst { send } = connect(ntp_ipv6_addres);\n\nconst ntp_clock_get_gmt_milli = \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00TSYNC\".map((v) =\u003e String.fromCharCode(v)).join('');\nsend(ntp_clock_get_gmt_milli, (_, timemi) =\u003e {\n    console.log(timemi);\n})\n```\n\n**The outcome of which is still:**\n```sh\nPS \u003e .\\test_stomped.exe                                            \n\u003e\u003e\u003e evil payload\n```\n\n**And to prying eyes:**\n\u003cp align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/dariushoule/node-sea-scallop/main/stomped.png\" alt=\"hexdump\"\u003e\u003c/p\u003e\n\nImportant Notes:\n1. Content is repacked in-place.\n2. The Code cache is NOT cleared when using this configuration, and _will_ be executed preferentially.\n    - The code cache's `kSourceHash` is recalculated to allow v8's `SanityCheckJustSource` check to pass. \n3. The target binary must have a code cache to be stompable.\n4. *If your SEA is code signed, repacking will make the signature invalid. You'll need to be able to resign the binary to make it valid. If your SEA is not codesigned, everything will work as expected.*\n\n\n#### Footnote: A Few Words on Script Stomping 🥾\n\nnodejs stores main code resources as either a plaintext string or a near-plaintext V8 snapshot inside its SEA blob. Along side the code, there is an optional bytecode cache that can be used to speed up compilation and execution. Assuming  source and bytecode pass sanity checks the bytecode will be used preferentially to the main code resource for execution. \n\nBy keeping the bytecode cache intact and replacing the main code resource, a desynchronization between source and bytecode is created. This allows a SEA to disguise the true intent of its main code resource, and stealth its logic behind a harder-to-reverse-engineer serialized v8 bytecode blob.\n\nThe implications are not altogether that different than the classic VBA/P-code Stomp: [https://attack.mitre.org/techniques/T1564/007/](https://attack.mitre.org/techniques/T1564/007/)\n\nI personally have used script stomped node SEAs as a very effective C2 implant delivery mechanism during red-team engagements. EDRs are not yet well clued into script stomping in SEAs.\n\n\n### Repack Asset\n\nRepack asset replaces a specific asset with a file of your choosing, creating it if it does not exist.\n\n```bash\nscallop repack-asset \u003ctarget_sea_binary\u003e \u003creplacement_asset_name\u003e \u003creplacement_asset_file\u003e\n```\n\nImportant Notes:\n1. Content is repacked in-place.\n2. The Code cache is cleared by default when using this configuration.\n3. *If your SEA is code signed, repacking will make the signature invalid. You'll need to be able to resign the binary to make it valid. If your SEA is not codesigned, everything will work as expected.*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdariushoule%2Fnode-sea-scallop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdariushoule%2Fnode-sea-scallop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdariushoule%2Fnode-sea-scallop/lists"}