Skip to content

Commit 6d621e0

Browse files
authored
Merge pull request #74 from hakril/improve_injection_exceptions
Improve injection error reporting
2 parents f80c473 + 98abd73 commit 6d621e0

3 files changed

Lines changed: 172 additions & 24 deletions

File tree

tests/test_injection.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# -*- coding: utf-8 -*-
2+
import pytest
3+
4+
import weakref
5+
import shutil
6+
import time
7+
import os
8+
9+
import windows
10+
import windows.generated_def as gdef
11+
12+
from .conftest import pop_proc_32, pop_proc_64
13+
from .pfwtest import DEFAULT_CREATION_FLAGS
14+
15+
@pytest.fixture(params=
16+
[(pop_proc_32, DEFAULT_CREATION_FLAGS),
17+
(pop_proc_32, gdef.CREATE_SUSPENDED),
18+
(pop_proc_64, DEFAULT_CREATION_FLAGS),
19+
(pop_proc_64, gdef.CREATE_SUSPENDED)],
20+
ids=["proc32", "proc32susp", "proc64", "proc64susp"])
21+
def proc_3264_runsus(request):
22+
"""Fixture for process 32/64 both running & suspended"""
23+
proc_poper, dwCreationFlags = request.param
24+
proc = proc_poper(dwCreationFlags=dwCreationFlags)
25+
time.sleep(0.2) # Give time to the process to load :)
26+
print("Created {0} ({1}bits) for test".format(proc, proc.bitness))
27+
yield weakref.proxy(proc) # provide the fixture value
28+
try:
29+
proc.exit(0)
30+
except WindowsError as e:
31+
if not proc.is_exit:
32+
raise
33+
# print("DEL PROC")
34+
del proc
35+
36+
# Its really the same test as test_process.test_load_library but with suspended process as well
37+
def test_dll_injection(proc_3264_runsus):
38+
assert (not proc_3264_runsus.peb.Ldr) or ("wintrust.dll" not in [mod.name for mod in proc_3264_runsus.peb.modules])
39+
modaddr = windows.injection.load_dll_in_remote_process(proc_3264_runsus, "wintrust.dll")
40+
wintrustmod = [mod for mod in proc_3264_runsus.peb.modules if mod.name == "wintrust.dll"][0]
41+
assert wintrustmod.baseaddr == modaddr
42+
43+
def test_dll_injection_error_reporting(proc_3264_runsus):
44+
with pytest.raises(windows.injection.InjectionFailedError) as excinfo:
45+
windows.injection.load_dll_in_remote_process(proc_3264_runsus, "NO_A_DLL.dll")
46+
assert excinfo.value.__cause__.winerror == gdef.ERROR_MOD_NOT_FOUND
47+
48+
def test_dll_injection_access_denied(proc_3264_runsus, tmpdir):
49+
"""Emulate injection of MsStore python, were its DLL are not executable by any other append
50+
See: https://github.com/hakril/PythonForWindows/issues/72
51+
"""
52+
mybitness = windows.current_process.bitness
53+
if proc_3264_runsus.bitness == mybitness:
54+
DLLPATH = r"c:\windows\system32\wintrust.dll"
55+
elif mybitness == 64: # target is 32
56+
DLLPATH = r"c:\windows\syswow64\wintrust.dll"
57+
elif mybitness == 32: # target is 64
58+
DLLPATH = r"c:\windows\sysnative\wintrust.dll"
59+
else:
60+
raise Value("WTF ARE THE BITNESS ?")
61+
targetname = os.path.join(str(tmpdir), "wintrust_noexec.dll")
62+
shutil.copy(DLLPATH, targetname)
63+
# Deny Execute; allow read for everyone
64+
sd = windows.security.SecurityDescriptor.from_string("D:(D;;GXFX;;;WD)(A;;1;;;WD)")
65+
sd.to_filename(targetname)
66+
67+
try:
68+
with pytest.raises(windows.injection.InjectionFailedError) as excinfo:
69+
windows.injection.load_dll_in_remote_process(proc_3264_runsus, targetname)
70+
assert excinfo.value.__cause__.winerror == gdef.ERROR_ACCESS_DENIED
71+
finally:
72+
proc_3264_runsus.exit()
73+
proc_3264_runsus.wait()
74+
time.sleep(0.5) # Fail on Azure CI of no sleep
75+
os.unlink(targetname)

tests/test_process.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,11 @@ def test_load_library(self, proc32_64):
370370
proc32_64.load_library(DLL)
371371
assert DLL in [m.name for m in proc32_64.peb.modules]
372372

373+
def test_load_library_suspended(self, proc32_64_suspended):
374+
DLL = "wintrust.dll"
375+
proc32_64_suspended.load_library(DLL)
376+
assert DLL in [m.name for m in proc32_64_suspended.peb.modules]
377+
373378
def test_load_library_unicode_name(self, proc32_64, tmpdir):
374379
mybitness = windows.current_process.bitness
375380
UNICODE_FILENAME = u'\u4e2d\u56fd\u94f6\u884c\u7f51\u94f6\u52a9\u624b.dll'

windows/injection.py

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ def perform_manual_getproc_loadlib_32(target, dll_name):
4040
code += x86.Call(":FUNC_GETPROCADDRESS32")
4141
code += x86.Push(x86.mem("[ECX + 8]"))
4242
code += x86.Call("EAX") # LoadLibrary
43+
code += x86.Cmp("EAX", 0)
44+
code += x86.Jnz(":end")
45+
# GetLastError()
46+
# I really don't want to resolve another function
47+
# For a field that have been the same since XP/Win2003
48+
code += x86.Mov('EAX', x86.mem('fs:[0x34]'))
49+
code += x86.Add("EAX", 0x80000000)
50+
code += x86.Label(":end")
4351
code += x86.Pop("ECX")
4452
code += x86.Pop("ECX")
4553
code += x86.Ret()
@@ -60,9 +68,18 @@ def perform_manual_getproc_loadlib_32(target, dll_name):
6068

6169
t = target.execute(RemoteManualLoadLibray.get_code(), addr4)
6270
t.wait()
63-
if not t.exit_code:
64-
raise InjectionFailedError("Injection of <{0}> failed".format(dll_name))
65-
return True
71+
module_baseaddr = t.exit_code
72+
73+
if module_baseaddr & 0x80000000:
74+
# Not a possible userland addr -> its a GetLastError()
75+
error_code = module_baseaddr & 0x7fffffff
76+
module_baseaddr = None
77+
real_error = ctypes.WinError(error_code)
78+
myexc = InjectionFailedError(u"Injection of <{0}> failed due to error <{1}> in injected process".format(dll_name, str(real_error)))
79+
myexc.__cause__ = real_error
80+
raise myexc
81+
82+
return module_baseaddr
6683

6784
def perform_manual_getproc_loadlib_64(target, dll_name):
6885
dll = get_kernel32_dll_name()
@@ -76,18 +93,23 @@ def perform_manual_getproc_loadlib_64(target, dll_name):
7693
code += x64.Mov("RDX", x64.mem("[R15 + 8]"))
7794
code += x64.Call(":FUNC_GETPROCADDRESS64")
7895
code += x64.Mov("RCX", x64.mem("[R15 + 0x10]"))
79-
code += x64.Push("RCX")
80-
code += x64.Push("RCX")
81-
code += x64.Push("RCX")
96+
code += (x64.Push("RCX") * 3)
8297
code += x64.Call("RAX") # LoadLibrary
83-
code += x64.Pop("RCX")
84-
code += x64.Pop("RCX")
85-
code += x64.Pop("RCX")
98+
code += (x64.Pop("RCX") * 3)
99+
code += x64.Mov("RCX", x64.mem("[R15]"))
100+
code += x64.Mov(x64.mem("[RCX]"), "RAX")
101+
# GetLastError()
102+
# I really don't want to resolve another function
103+
# For a field that have been the same since XP/Win2003
104+
code += x64.Mov('RAX', x64.mem('gs:[0x68]'))
86105
code += x64.Ret()
87106

88107
RemoteManualLoadLibray += GetProcAddress64
89108

90109
with target.allocated_memory(0x1000) as addr:
110+
# Addr contains the name of kernel32
111+
# The data at addr are discadable after the call
112+
# So, on return it contains the return PVOID64 value of LoadLibraryA
91113
addr2 = addr + len(dll)
92114
addr3 = addr2 + len(api)
93115
addr4 = addr3 + len(dll_to_load)
@@ -101,17 +123,55 @@ def perform_manual_getproc_loadlib_64(target, dll_name):
101123

102124
t = target.execute(RemoteManualLoadLibray.get_code(), addr4)
103125
t.wait()
104-
if not t.exit_code:
105-
raise InjectionFailedError("Injection of <{0}> failed".format(dll_name))
106-
return True
126+
module_baseaddr = target.read_ptr(addr)
127+
if not module_baseaddr:
128+
module_baseaddr = None
129+
real_error = ctypes.WinError(t.exit_code)
130+
myexc = InjectionFailedError(u"Injection of <{0}> failed due to error <{1}> in injected process".format(dll_name, str(real_error)))
131+
myexc.__cause__ = real_error
132+
raise myexc
133+
134+
return module_baseaddr
135+
136+
def generate_simple_LoadLibraryW_32_with_error(k32):
137+
"""A shellcode that execute LoadLibraryW(param) and returns the value.
138+
If LoadLibraryW fails -> returns (GetLastError | 0x10000000)
107139
108-
def generate_simple_LoadLibraryW_64(load_libraryW, remote_store):
140+
As a valid 32b modules will never be in >=0x80000000,
141+
this allow to determine if the call was successful of not"""
142+
load_libraryW = k32.pe.exports["LoadLibraryW"]
143+
GetLastError = k32.pe.exports["GetLastError"]
144+
145+
code = x86.MultipleInstr()
146+
code += x86.Mov("EAX", x86.mem("[ESP + 4]"))
147+
code += x86.Push("EAX")
148+
code += x86.Mov("EAX", load_libraryW)
149+
code += x86.Call("EAX")
150+
code += x86.Cmp("EAX", 0)
151+
code += x86.Jnz(":end")
152+
code += x86.Mov("EAX", GetLastError)
153+
code += x86.Call("EAX")
154+
code += x86.Add("EAX", 0x80000000)
155+
code += x86.Label(":end")
156+
code += x86.Ret()
157+
return code.get_code()
158+
159+
def generate_simple_LoadLibraryW_64_with_error(k32, remote_store):
160+
"""A shellcode that execute LoadLibraryW(param) and store the value at a fixed address.
161+
This allow a 32b process to inject and retrieve a 64bit module address
162+
163+
Thread return value is the result of GetLastError()
164+
"""
165+
load_libraryW = k32.pe.exports["LoadLibraryW"]
166+
GetLastError = k32.pe.exports["GetLastError"]
109167
code = RemoteLoadLibrayStub = x64.MultipleInstr()
110168
code += x64.Mov("RAX", load_libraryW)
111169
code += (x64.Push("RDI") * 5) # Prepare stack
112170
code += x64.Call("RAX")
113-
code += (x64.Pop("RDI") * 5) # Clean stack
114171
code += x64.Mov(x64.deref(remote_store), "RAX")
172+
code += x64.Mov("RAX", GetLastError) # Add a jump ?
173+
code += x64.Call("RAX")
174+
code += (x64.Pop("RDI") * 5) # Clean stack
115175
code += x64.Ret()
116176
return RemoteLoadLibrayStub.get_code()
117177

@@ -136,17 +196,21 @@ def load_dll_in_remote_process(target, dll_path):
136196
if k32:
137197
# We have kernel32 \o/
138198
k32 = k32[0]
139-
try:
140-
load_libraryW = k32.pe.exports["LoadLibraryW"]
141-
except KeyError:
142-
raise ValueError("Kernel32 have no export <LoadLibraryA> (wtf)")
143-
144199
with target.allocated_memory(0x1000) as addr:
145200
if target.bitness == 32:
146-
target.write_memory(addr, (dll_path + "\x00").encode('utf-16le'))
147-
t = target.create_thread(load_libraryW, addr)
201+
shellcode32 = generate_simple_LoadLibraryW_32_with_error(k32)
202+
encoded_dll_name = (dll_path + "\x00").encode('utf-16le')
203+
paramaddr = addr
204+
target.write_memory(addr, encoded_dll_name)
205+
shellcode_addr = addr + len(encoded_dll_name)
206+
target.write_memory(shellcode_addr, shellcode32)
207+
t = target.create_thread(shellcode_addr, paramaddr)
148208
t.wait()
149-
module_baseaddr = t.exit_code
209+
exit_code = module_baseaddr = t.exit_code
210+
if module_baseaddr & 0x80000000:
211+
# Not a possible userland addr -> its a GetLastError()
212+
module_baseaddr = None
213+
exit_code = exit_code & 0x7fffffff
150214
else:
151215
# For 64b target we need a special stub as the return value of
152216
# load_libraryW does not fit in t.exit_code (DWORD)
@@ -158,14 +222,18 @@ def load_dll_in_remote_process(target, dll_path):
158222
param_addr = addr
159223
addr += len(full_dll_name)
160224
shellcode_addr = addr
161-
shellcode = generate_simple_LoadLibraryW_64(load_libraryW, retval_addr)
225+
shellcode = generate_simple_LoadLibraryW_64_with_error(k32, retval_addr)
162226
target.write_memory(shellcode_addr, shellcode)
163227
t = target.create_thread(shellcode_addr, param_addr)
164228
t.wait()
229+
exit_code = t.exit_code
165230
module_baseaddr = target.read_ptr(retval_addr)
166231

167232
if not module_baseaddr:
168-
raise InjectionFailedError(u"Injection of <{0}> failed".format(dll_path))
233+
real_error = ctypes.WinError(exit_code)
234+
myexc = InjectionFailedError(u"Injection of <{0}> failed due to error <{1}> in injected process".format(dll_path, str(real_error)))
235+
myexc.__cause__ = real_error
236+
raise myexc
169237
dbgprint("DLL Injected via LoadLibray", "DLLINJECT")
170238
# Cannot return the full return value of load_libraryW in 64b target.. (exit_code is a DWORD)
171239
return module_baseaddr

0 commit comments

Comments
 (0)