Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ on:
pull_request:
branches: [master]

permissions: read-all

jobs:
lint:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions speakeasy/profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def __init__(self):
self.coverage: set[int] = set()
self.memory_regions: list[dict[str, Any]] = []
self.loaded_modules: list[dict[str, Any]] = []
self.init_regs: dict[int, int] = {}

def get_api_count(self):
"""
Expand Down
2 changes: 1 addition & 1 deletion speakeasy/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.0.0a1"
__version__ = "2.0.0b1"
135 changes: 75 additions & 60 deletions speakeasy/windows/win32.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ def prepare_module_for_emulation(self, module, all_entrypoints):
self.stop()
raise Win32EmuError("Module not found")

# Check if any TLS callbacks exist, these run before the module's entry point
runs = []

tls = module.get_tls_callbacks()
for i, cb_addr in enumerate(tls):
base = module.base
Expand All @@ -232,6 +233,7 @@ def prepare_module_for_emulation(self, module, all_entrypoints):
run.type = f"tls_callback_{i}"
run.args = [base, DLL_PROCESS_ATTACH, 0]
self.add_run(run)
runs.append(run)

ep = module.base + module.ep

Expand All @@ -241,20 +243,14 @@ def prepare_module_for_emulation(self, module, all_entrypoints):
if not module.is_exe():
run.args = [module.base, DLL_PROCESS_ATTACH, 0]
run.type = "dll_entry.DLL_PROCESS_ATTACH"
container = self.init_container_process()
if container:
self.processes.append(container)
self.curr_process = container
else:
run.type = "module_entry"
run.args = [self.mem_map(8, tag=f"emu.module_arg_{i}") for i in range(4)]

self.add_run(run)
runs.append(run)

if all_entrypoints:
# Only emulate a subset of all the exported functions
# There are some modules (such as the windows kernel) with
# thousands of exports
exports = [k for k in module.get_exports()[:MAX_EXPORTS_TO_EMULATE]]

if exports:
Expand All @@ -275,14 +271,11 @@ def prepare_module_for_emulation(self, module, all_entrypoints):
argc, argv = self.build_service_main_args("IPRIP", char_width=char_width)
run.args = [argc, argv]
else:
# Here we set dummy args to pass into the export function
run.args = args
# Store these runs and only queue them before the unload
# routine this is because some exports may not be ready to
# be called yet
self.add_run(run)
runs.append(run)

return
return runs

def run_module(self, module, all_entrypoints=False, emulate_children=False):
"""
Expand All @@ -291,59 +284,57 @@ def run_module(self, module, all_entrypoints=False, emulate_children=False):
Arguments:
module: Module to emulate
"""
self.prepare_module_for_emulation(module, all_entrypoints)
runs = self.prepare_module_for_emulation(module, all_entrypoints)

# Create an empty process object for the module if none is
# supplied, only do this for the main module
if len(self.processes) == 0:
if not module.is_exe():
container = self.init_container_process()
if container:
p = container
else:
p = objman.Process(self, path=module.emu_path, base=module.base, pe=module, cmdline=self.command_line)
else:
p = objman.Process(self, path=module.emu_path, base=module.base, pe=module, cmdline=self.command_line)
self.curr_process = p
self.om.objects.update({p.address: p}) # type: ignore[union-attr]
mm = self.get_address_map(module.base)
if mm:
mm.process = self.curr_process

t = objman.Thread(self, stack_base=self.stack_base, stack_commit=module.stack_commit)
self.processes.append(p)
self.om.objects.update({p.address: p}) # type: ignore[union-attr]
mm = self.get_address_map(module.base)
if mm:
mm.process = p

t = objman.Thread(self, stack_base=self.stack_base, stack_commit=module.stack_commit)
self.om.objects.update({t.address: t}) # type: ignore[union-attr]
self.curr_process.threads.append(t) # type: ignore[union-attr]
self.curr_thread = t

if self.run_queue:
self.run_queue[0].thread = t
t.process = p
p.threads.append(t)

peb = self.alloc_peb(self.curr_process)
for r in runs:
r.process_context = p
r.thread = t

# Set the TEB
self.init_teb(t, peb)

# Begin emulation of main module
self.start()

if not emulate_children or len(self.child_processes) == 0:
return

# Emulate any child processes
while len(self.child_processes) > 0:
child = self.child_processes.pop(0)

child.pe = self.load_module(data=child.pe_data)
self.prepare_module_for_emulation(child.pe, all_entrypoints)
child_runs = self.prepare_module_for_emulation(child.pe, all_entrypoints)

self.command_line = child.cmdline
child.base = child.pe.base

self.curr_process = child
self.curr_process.base = child.pe.base
self.curr_thread = child.threads[0]
self.processes.append(child)

self.om.objects.update({self.curr_thread.address: self.curr_thread}) # type: ignore[union-attr]
child_thread = child.threads[0]
self.om.objects.update({child_thread.address: child_thread}) # type: ignore[union-attr]

# PEB and TEB will be initialized when the next run happens
for r in child_runs:
r.process_context = child
r.thread = child_thread

self.start()

return

def _init_name(self, path, data=None):
if not data:
self.file_name = os.path.basename(path)
Expand Down Expand Up @@ -419,45 +410,37 @@ def run_shellcode(self, sc_addr, stack_commit=0x4000, offset=0):
if not target:
raise Win32EmuError("Invalid shellcode address")

self.stack_base, stack_addr = self.alloc_stack(stack_commit)
self.set_func_args(self.stack_base, self.return_hook, 0x7000)
self.stack_base, _ = self.alloc_stack(stack_commit)

run = Run()
run.type = "shellcode"
run.start_addr = sc_addr + offset
run.instr_cnt = 0
args = [self.mem_map(1024, tag=f"emu.shellcode_arg_{i}", base=0x41420000 + i) for i in range(4)]
run.args = args
run.init_regs = {_arch.X86_REG_ECX: 1024}

self.reg_write(_arch.X86_REG_ECX, 1024)

self.add_run(run)

# Create an empty process object for the shellcode if none is
# supplied
container = self.init_container_process()
if container:
self.processes.append(container)
self.curr_process = container
p = container
else:
p = objman.Process(self)
self.processes.append(p)
self.curr_process = p

self.om.objects.update({p.address: p}) # type: ignore[union-attr]
mm = self.get_address_map(sc_addr)
if mm:
mm.process = self.curr_process
mm.process = p

t = objman.Thread(self, stack_base=self.stack_base, stack_commit=stack_commit)
self.om.objects.update({t.address: t}) # type: ignore[union-attr]
self.curr_process.threads.append(t)

self.curr_thread = t

peb = self.alloc_peb(self.curr_process)
t.process = p
p.threads.append(t)

# Set the TEB
self.init_teb(t, peb)
run.process_context = p
run.thread = t
self.add_run(run)

self.start()

Expand Down Expand Up @@ -575,6 +558,38 @@ def init_container_process(self):
return proc
return None

def _create_default_process(self, run):
mod = self.get_module_from_addr(run.start_addr)

if mod and getattr(mod, "is_exe", lambda: False)():
p = objman.Process(
self,
path=getattr(mod, "emu_path", ""),
base=getattr(mod, "base", 0),
pe=mod,
cmdline=self.command_line,
)
else:
container = self.init_container_process()
if container:
p = container
elif mod:
p = objman.Process(
self,
path=getattr(mod, "emu_path", ""),
base=getattr(mod, "base", 0),
pe=mod,
)
else:
p = objman.Process(self)

self.processes.append(p)
self.om.objects.update({p.address: p}) # type: ignore[union-attr]
mm = self.get_address_map(run.start_addr)
if mm:
mm.process = p
return p

def _init_user_modules_from_config(self):
proc_mod = None
for p in self.config.processes:
Expand Down
71 changes: 52 additions & 19 deletions speakeasy/windows/winemu.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,6 @@ def call(self, addr, params=[]):
"""
Start emulating at the specified address
"""
self.reset_stack(self.stack_base)
run = Run()
run.type = f"call_0x{addr:x}"
run.start_addr = addr
Expand All @@ -415,9 +414,50 @@ def call(self, addr, params=[]):
else:
self.add_run(run)

def _resolve_run_process(self, run):
if run.process_context:
return run.process_context
if run.thread and getattr(run.thread, "process", None):
return run.thread.process
if self.curr_process:
return self.curr_process
return self._create_default_process(run)

def _create_default_process(self, run):
p = objman.Process(self)
self.processes.append(p)
self.om.objects.update({p.address: p}) # type: ignore[union-attr]
return p

def _resolve_run_thread(self, run, proc):
if run.thread:
tp = getattr(run.thread, "process", None)
if tp is None:
run.thread.process = proc
if run.thread not in proc.threads:
proc.threads.append(run.thread)
elif tp is not proc:
raise WindowsEmuError(
f"Run thread is bound to a different process "
f"(thread.process={tp!r}, resolved={proc!r})"
)
return run.thread
if self.kernel_mode:
return None
thread = objman.Thread(self, stack_base=self.stack_base)
self.om.objects.update({thread.address: thread}) # type: ignore[union-attr]
thread.process = proc
proc.threads.append(thread)
run.thread = thread
return thread

def _prepare_run_context(self, run):
"""
Prepare CPU and memory state for the given run without starting emulation.

This is the single canonical path for process/thread/PEB/TEB/TLS
activation. All run types (call, module entry, shellcode, thread)
converge here.
"""
logger.info("* exec: %s", run.type)

Expand All @@ -430,30 +470,24 @@ def _prepare_run_context(self, run):
stk_ptr = self.get_stack_ptr()

self.set_func_args(stk_ptr, self.return_hook, *run.args)
for reg, val in run.init_regs.items():
self.reg_write(reg, val)
stk_ptr = self.get_stack_ptr()
stk_map = self.get_address_map(stk_ptr)

self.curr_run.stack = MemAccess(base=stk_map.base, size=stk_map.size)

# Set the process context if possible
if run.process_context:
# Init a new peb if the process context changed:
if run.process_context != self.get_current_process():
self.alloc_peb(run.process_context)
self.set_current_process(run.process_context)
if run.thread:
self.set_current_thread(run.thread)
elif not self.kernel_mode:
thread = objman.Thread(self, stack_base=self.stack_base)
self.om.objects.update({thread.address: thread})
if self.curr_process:
thread.process = self.curr_process
self.curr_process.threads.append(thread)
run.thread = thread
proc = self._resolve_run_process(run)
run.process_context = proc
self.set_current_process(proc)
self.alloc_peb(proc)

thread = self._resolve_run_thread(run, proc)
if thread:
self.set_current_thread(thread)
run.thread = thread

if not self.kernel_mode:
# Reset the TIB data
thread = self.get_current_thread()
if thread:
self.init_teb(thread, self.curr_process.peb) # type: ignore[union-attr]
Expand Down Expand Up @@ -517,8 +551,7 @@ def start(self, addr=None, size=None):
self.set_hooks()
self._set_emu_hooks()

# Initialize run context/register state before exposing the target to GDB,
# so the first stop reports a meaningful PC/SP/etc.
self.reset_stack(self.stack_base)
self._prepare_run_context(run)

if self.gdb_port is not None:
Expand Down
Loading
Loading