|
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
|
2 // Use of this source code is governed by a BSD-style license that can be |
|
3 // found in the LICENSE file. |
|
4 |
|
5 // For information about interceptions as a whole see |
|
6 // http://dev.chromium.org/developers/design-documents/sandbox . |
|
7 |
|
8 #include <set> |
|
9 |
|
10 #include "sandbox/win/src/interception.h" |
|
11 |
|
12 #include "base/logging.h" |
|
13 #include "base/memory/scoped_ptr.h" |
|
14 #include "base/win/pe_image.h" |
|
15 #include "base/win/windows_version.h" |
|
16 #include "sandbox/win/src/interception_internal.h" |
|
17 #include "sandbox/win/src/interceptors.h" |
|
18 #include "sandbox/win/src/sandbox.h" |
|
19 #include "sandbox/win/src/sandbox_utils.h" |
|
20 #include "sandbox/win/src/service_resolver.h" |
|
21 #include "sandbox/win/src/target_interceptions.h" |
|
22 #include "sandbox/win/src/target_process.h" |
|
23 #include "sandbox/win/src/wow64.h" |
|
24 |
|
25 namespace { |
|
26 |
|
27 const char kMapViewOfSectionName[] = "NtMapViewOfSection"; |
|
28 const char kUnmapViewOfSectionName[] = "NtUnmapViewOfSection"; |
|
29 |
|
30 // Standard allocation granularity and page size for Windows. |
|
31 const size_t kAllocGranularity = 65536; |
|
32 const size_t kPageSize = 4096; |
|
33 |
|
34 // Find a random offset within 64k and aligned to ceil(log2(size)). |
|
35 size_t GetGranularAlignedRandomOffset(size_t size) { |
|
36 CHECK_LE(size, kAllocGranularity); |
|
37 unsigned int offset; |
|
38 |
|
39 do { |
|
40 rand_s(&offset); |
|
41 offset &= (kAllocGranularity - 1); |
|
42 } while (offset > (kAllocGranularity - size)); |
|
43 |
|
44 // Find an alignment between 64 and the page size (4096). |
|
45 size_t align_size = kPageSize; |
|
46 for (size_t new_size = align_size / 2; new_size >= size; new_size /= 2) { |
|
47 align_size = new_size; |
|
48 } |
|
49 return offset & ~(align_size - 1); |
|
50 } |
|
51 |
|
52 } // namespace |
|
53 |
|
54 namespace sandbox { |
|
55 |
|
56 SANDBOX_INTERCEPT SharedMemory* g_interceptions; |
|
57 |
|
58 // Table of the unpatched functions that we intercept. Mapped from the parent. |
|
59 SANDBOX_INTERCEPT OriginalFunctions g_originals = { NULL }; |
|
60 |
|
61 // Magic constant that identifies that this function is not to be patched. |
|
62 const char kUnloadDLLDummyFunction[] = "@"; |
|
63 |
|
64 InterceptionManager::InterceptionManager(TargetProcess* child_process, |
|
65 bool relaxed) |
|
66 : child_(child_process), names_used_(false), relaxed_(relaxed) { |
|
67 child_->AddRef(); |
|
68 } |
|
69 InterceptionManager::~InterceptionManager() { |
|
70 child_->Release(); |
|
71 } |
|
72 |
|
73 bool InterceptionManager::AddToPatchedFunctions( |
|
74 const wchar_t* dll_name, const char* function_name, |
|
75 InterceptionType interception_type, const void* replacement_code_address, |
|
76 InterceptorId id) { |
|
77 InterceptionData function; |
|
78 function.type = interception_type; |
|
79 function.id = id; |
|
80 function.dll = dll_name; |
|
81 function.function = function_name; |
|
82 function.interceptor_address = replacement_code_address; |
|
83 |
|
84 interceptions_.push_back(function); |
|
85 return true; |
|
86 } |
|
87 |
|
88 bool InterceptionManager::AddToPatchedFunctions( |
|
89 const wchar_t* dll_name, const char* function_name, |
|
90 InterceptionType interception_type, const char* replacement_function_name, |
|
91 InterceptorId id) { |
|
92 InterceptionData function; |
|
93 function.type = interception_type; |
|
94 function.id = id; |
|
95 function.dll = dll_name; |
|
96 function.function = function_name; |
|
97 function.interceptor = replacement_function_name; |
|
98 function.interceptor_address = NULL; |
|
99 |
|
100 interceptions_.push_back(function); |
|
101 names_used_ = true; |
|
102 return true; |
|
103 } |
|
104 |
|
105 bool InterceptionManager::AddToUnloadModules(const wchar_t* dll_name) { |
|
106 InterceptionData module_to_unload; |
|
107 module_to_unload.type = INTERCEPTION_UNLOAD_MODULE; |
|
108 module_to_unload.dll = dll_name; |
|
109 // The next two are dummy values that make the structures regular, instead |
|
110 // of having special cases. They should not be used. |
|
111 module_to_unload.function = kUnloadDLLDummyFunction; |
|
112 module_to_unload.interceptor_address = reinterpret_cast<void*>(1); |
|
113 |
|
114 interceptions_.push_back(module_to_unload); |
|
115 return true; |
|
116 } |
|
117 |
|
118 bool InterceptionManager::InitializeInterceptions() { |
|
119 if (interceptions_.empty()) |
|
120 return true; // Nothing to do here |
|
121 |
|
122 size_t buffer_bytes = GetBufferSize(); |
|
123 scoped_ptr<char[]> local_buffer(new char[buffer_bytes]); |
|
124 |
|
125 if (!SetupConfigBuffer(local_buffer.get(), buffer_bytes)) |
|
126 return false; |
|
127 |
|
128 void* remote_buffer; |
|
129 if (!CopyDataToChild(local_buffer.get(), buffer_bytes, &remote_buffer)) |
|
130 return false; |
|
131 |
|
132 bool hot_patch_needed = (0 != buffer_bytes); |
|
133 if (!PatchNtdll(hot_patch_needed)) |
|
134 return false; |
|
135 |
|
136 g_interceptions = reinterpret_cast<SharedMemory*>(remote_buffer); |
|
137 ResultCode rc = child_->TransferVariable("g_interceptions", |
|
138 &g_interceptions, |
|
139 sizeof(g_interceptions)); |
|
140 return (SBOX_ALL_OK == rc); |
|
141 } |
|
142 |
|
143 size_t InterceptionManager::GetBufferSize() const { |
|
144 std::set<std::wstring> dlls; |
|
145 size_t buffer_bytes = 0; |
|
146 |
|
147 std::list<InterceptionData>::const_iterator it = interceptions_.begin(); |
|
148 for (; it != interceptions_.end(); ++it) { |
|
149 // skip interceptions that are performed from the parent |
|
150 if (!IsInterceptionPerformedByChild(*it)) |
|
151 continue; |
|
152 |
|
153 if (!dlls.count(it->dll)) { |
|
154 // NULL terminate the dll name on the structure |
|
155 size_t dll_name_bytes = (it->dll.size() + 1) * sizeof(wchar_t); |
|
156 |
|
157 // include the dll related size |
|
158 buffer_bytes += RoundUpToMultiple(offsetof(DllPatchInfo, dll_name) + |
|
159 dll_name_bytes, sizeof(size_t)); |
|
160 dlls.insert(it->dll); |
|
161 } |
|
162 |
|
163 // we have to NULL terminate the strings on the structure |
|
164 size_t strings_chars = it->function.size() + it->interceptor.size() + 2; |
|
165 |
|
166 // a new FunctionInfo is required per function |
|
167 size_t record_bytes = offsetof(FunctionInfo, function) + strings_chars; |
|
168 record_bytes = RoundUpToMultiple(record_bytes, sizeof(size_t)); |
|
169 buffer_bytes += record_bytes; |
|
170 } |
|
171 |
|
172 if (0 != buffer_bytes) |
|
173 // add the part of SharedMemory that we have not counted yet |
|
174 buffer_bytes += offsetof(SharedMemory, dll_list); |
|
175 |
|
176 return buffer_bytes; |
|
177 } |
|
178 |
|
179 // Basically, walk the list of interceptions moving them to the config buffer, |
|
180 // but keeping together all interceptions that belong to the same dll. |
|
181 // The config buffer is a local buffer, not the one allocated on the child. |
|
182 bool InterceptionManager::SetupConfigBuffer(void* buffer, size_t buffer_bytes) { |
|
183 if (0 == buffer_bytes) |
|
184 return true; |
|
185 |
|
186 DCHECK(buffer_bytes > sizeof(SharedMemory)); |
|
187 |
|
188 SharedMemory* shared_memory = reinterpret_cast<SharedMemory*>(buffer); |
|
189 DllPatchInfo* dll_info = shared_memory->dll_list; |
|
190 int num_dlls = 0; |
|
191 |
|
192 shared_memory->interceptor_base = names_used_ ? child_->MainModule() : NULL; |
|
193 |
|
194 buffer_bytes -= offsetof(SharedMemory, dll_list); |
|
195 buffer = dll_info; |
|
196 |
|
197 std::list<InterceptionData>::iterator it = interceptions_.begin(); |
|
198 for (; it != interceptions_.end();) { |
|
199 // skip interceptions that are performed from the parent |
|
200 if (!IsInterceptionPerformedByChild(*it)) { |
|
201 ++it; |
|
202 continue; |
|
203 } |
|
204 |
|
205 const std::wstring dll = it->dll; |
|
206 if (!SetupDllInfo(*it, &buffer, &buffer_bytes)) |
|
207 return false; |
|
208 |
|
209 // walk the interceptions from this point, saving the ones that are |
|
210 // performed on this dll, and removing the entry from the list. |
|
211 // advance the iterator before removing the element from the list |
|
212 std::list<InterceptionData>::iterator rest = it; |
|
213 for (; rest != interceptions_.end();) { |
|
214 if (rest->dll == dll) { |
|
215 if (!SetupInterceptionInfo(*rest, &buffer, &buffer_bytes, dll_info)) |
|
216 return false; |
|
217 if (it == rest) |
|
218 ++it; |
|
219 rest = interceptions_.erase(rest); |
|
220 } else { |
|
221 ++rest; |
|
222 } |
|
223 } |
|
224 dll_info = reinterpret_cast<DllPatchInfo*>(buffer); |
|
225 ++num_dlls; |
|
226 } |
|
227 |
|
228 shared_memory->num_intercepted_dlls = num_dlls; |
|
229 return true; |
|
230 } |
|
231 |
|
232 // Fills up just the part that depends on the dll, not the info that depends on |
|
233 // the actual interception. |
|
234 bool InterceptionManager::SetupDllInfo(const InterceptionData& data, |
|
235 void** buffer, |
|
236 size_t* buffer_bytes) const { |
|
237 DCHECK(buffer_bytes); |
|
238 DCHECK(buffer); |
|
239 DCHECK(*buffer); |
|
240 |
|
241 DllPatchInfo* dll_info = reinterpret_cast<DllPatchInfo*>(*buffer); |
|
242 |
|
243 // the strings have to be zero terminated |
|
244 size_t required = offsetof(DllPatchInfo, dll_name) + |
|
245 (data.dll.size() + 1) * sizeof(wchar_t); |
|
246 required = RoundUpToMultiple(required, sizeof(size_t)); |
|
247 if (*buffer_bytes < required) |
|
248 return false; |
|
249 |
|
250 *buffer_bytes -= required; |
|
251 *buffer = reinterpret_cast<char*>(*buffer) + required; |
|
252 |
|
253 // set up the dll info to be what we know about it at this time |
|
254 dll_info->unload_module = (data.type == INTERCEPTION_UNLOAD_MODULE); |
|
255 dll_info->record_bytes = required; |
|
256 dll_info->offset_to_functions = required; |
|
257 dll_info->num_functions = 0; |
|
258 data.dll._Copy_s(dll_info->dll_name, data.dll.size(), data.dll.size()); |
|
259 dll_info->dll_name[data.dll.size()] = L'\0'; |
|
260 |
|
261 return true; |
|
262 } |
|
263 |
|
264 bool InterceptionManager::SetupInterceptionInfo(const InterceptionData& data, |
|
265 void** buffer, |
|
266 size_t* buffer_bytes, |
|
267 DllPatchInfo* dll_info) const { |
|
268 DCHECK(buffer_bytes); |
|
269 DCHECK(buffer); |
|
270 DCHECK(*buffer); |
|
271 |
|
272 if ((dll_info->unload_module) && |
|
273 (data.function != kUnloadDLLDummyFunction)) { |
|
274 // Can't specify a dll for both patch and unload. |
|
275 NOTREACHED(); |
|
276 } |
|
277 |
|
278 FunctionInfo* function = reinterpret_cast<FunctionInfo*>(*buffer); |
|
279 |
|
280 size_t name_bytes = data.function.size(); |
|
281 size_t interceptor_bytes = data.interceptor.size(); |
|
282 |
|
283 // the strings at the end of the structure are zero terminated |
|
284 size_t required = offsetof(FunctionInfo, function) + |
|
285 name_bytes + interceptor_bytes + 2; |
|
286 required = RoundUpToMultiple(required, sizeof(size_t)); |
|
287 if (*buffer_bytes < required) |
|
288 return false; |
|
289 |
|
290 // update the caller's values |
|
291 *buffer_bytes -= required; |
|
292 *buffer = reinterpret_cast<char*>(*buffer) + required; |
|
293 |
|
294 function->record_bytes = required; |
|
295 function->type = data.type; |
|
296 function->id = data.id; |
|
297 function->interceptor_address = data.interceptor_address; |
|
298 char* names = function->function; |
|
299 |
|
300 data.function._Copy_s(names, name_bytes, name_bytes); |
|
301 names += name_bytes; |
|
302 *names++ = '\0'; |
|
303 |
|
304 // interceptor follows the function_name |
|
305 data.interceptor._Copy_s(names, interceptor_bytes, interceptor_bytes); |
|
306 names += interceptor_bytes; |
|
307 *names++ = '\0'; |
|
308 |
|
309 // update the dll table |
|
310 dll_info->num_functions++; |
|
311 dll_info->record_bytes += required; |
|
312 |
|
313 return true; |
|
314 } |
|
315 |
|
316 bool InterceptionManager::CopyDataToChild(const void* local_buffer, |
|
317 size_t buffer_bytes, |
|
318 void** remote_buffer) const { |
|
319 DCHECK(NULL != remote_buffer); |
|
320 if (0 == buffer_bytes) { |
|
321 *remote_buffer = NULL; |
|
322 return true; |
|
323 } |
|
324 |
|
325 HANDLE child = child_->Process(); |
|
326 |
|
327 // Allocate memory on the target process without specifying the address |
|
328 void* remote_data = ::VirtualAllocEx(child, NULL, buffer_bytes, |
|
329 MEM_COMMIT, PAGE_READWRITE); |
|
330 if (NULL == remote_data) |
|
331 return false; |
|
332 |
|
333 SIZE_T bytes_written; |
|
334 BOOL success = ::WriteProcessMemory(child, remote_data, local_buffer, |
|
335 buffer_bytes, &bytes_written); |
|
336 if (FALSE == success || bytes_written != buffer_bytes) { |
|
337 ::VirtualFreeEx(child, remote_data, 0, MEM_RELEASE); |
|
338 return false; |
|
339 } |
|
340 |
|
341 *remote_buffer = remote_data; |
|
342 |
|
343 return true; |
|
344 } |
|
345 |
|
346 // Only return true if the child should be able to perform this interception. |
|
347 bool InterceptionManager::IsInterceptionPerformedByChild( |
|
348 const InterceptionData& data) const { |
|
349 if (INTERCEPTION_INVALID == data.type) |
|
350 return false; |
|
351 |
|
352 if (INTERCEPTION_SERVICE_CALL == data.type) |
|
353 return false; |
|
354 |
|
355 if (data.type >= INTERCEPTION_LAST) |
|
356 return false; |
|
357 |
|
358 std::wstring ntdll(kNtdllName); |
|
359 if (ntdll == data.dll) |
|
360 return false; // ntdll has to be intercepted from the parent |
|
361 |
|
362 return true; |
|
363 } |
|
364 |
|
365 bool InterceptionManager::PatchNtdll(bool hot_patch_needed) { |
|
366 // Maybe there is nothing to do |
|
367 if (!hot_patch_needed && interceptions_.empty()) |
|
368 return true; |
|
369 |
|
370 if (hot_patch_needed) { |
|
371 #if SANDBOX_EXPORTS |
|
372 // Make sure the functions are not excluded by the linker. |
|
373 #if defined(_WIN64) |
|
374 #pragma comment(linker, "/include:TargetNtMapViewOfSection64") |
|
375 #pragma comment(linker, "/include:TargetNtUnmapViewOfSection64") |
|
376 #else |
|
377 #pragma comment(linker, "/include:_TargetNtMapViewOfSection@44") |
|
378 #pragma comment(linker, "/include:_TargetNtUnmapViewOfSection@12") |
|
379 #endif |
|
380 #endif |
|
381 ADD_NT_INTERCEPTION(NtMapViewOfSection, MAP_VIEW_OF_SECTION_ID, 44); |
|
382 ADD_NT_INTERCEPTION(NtUnmapViewOfSection, UNMAP_VIEW_OF_SECTION_ID, 12); |
|
383 } |
|
384 |
|
385 // Reserve a full 64k memory range in the child process. |
|
386 HANDLE child = child_->Process(); |
|
387 BYTE* thunk_base = reinterpret_cast<BYTE*>( |
|
388 ::VirtualAllocEx(child, NULL, kAllocGranularity, |
|
389 MEM_RESERVE, PAGE_NOACCESS)); |
|
390 |
|
391 // Find an aligned, random location within the reserved range. |
|
392 size_t thunk_bytes = interceptions_.size() * sizeof(ThunkData) + |
|
393 sizeof(DllInterceptionData); |
|
394 size_t thunk_offset = GetGranularAlignedRandomOffset(thunk_bytes); |
|
395 |
|
396 // Split the base and offset along page boundaries. |
|
397 thunk_base += thunk_offset & ~(kPageSize - 1); |
|
398 thunk_offset &= kPageSize - 1; |
|
399 |
|
400 // Make an aligned, padded allocation, and move the pointer to our chunk. |
|
401 size_t thunk_bytes_padded = (thunk_bytes + kPageSize - 1) & kPageSize; |
|
402 thunk_base = reinterpret_cast<BYTE*>( |
|
403 ::VirtualAllocEx(child, thunk_base, thunk_bytes_padded, |
|
404 MEM_COMMIT, PAGE_EXECUTE_READWRITE)); |
|
405 CHECK(thunk_base); // If this fails we'd crash anyway on an invalid access. |
|
406 DllInterceptionData* thunks = reinterpret_cast<DllInterceptionData*>( |
|
407 thunk_base + thunk_offset); |
|
408 |
|
409 DllInterceptionData dll_data; |
|
410 dll_data.data_bytes = thunk_bytes; |
|
411 dll_data.num_thunks = 0; |
|
412 dll_data.used_bytes = offsetof(DllInterceptionData, thunks); |
|
413 |
|
414 // Reset all helpers for a new child. |
|
415 memset(g_originals, 0, sizeof(g_originals)); |
|
416 |
|
417 // this should write all the individual thunks to the child's memory |
|
418 if (!PatchClientFunctions(thunks, thunk_bytes, &dll_data)) |
|
419 return false; |
|
420 |
|
421 // and now write the first part of the table to the child's memory |
|
422 SIZE_T written; |
|
423 bool ok = FALSE != ::WriteProcessMemory(child, thunks, &dll_data, |
|
424 offsetof(DllInterceptionData, thunks), |
|
425 &written); |
|
426 |
|
427 if (!ok || (offsetof(DllInterceptionData, thunks) != written)) |
|
428 return false; |
|
429 |
|
430 // Attempt to protect all the thunks, but ignore failure |
|
431 DWORD old_protection; |
|
432 ::VirtualProtectEx(child, thunks, thunk_bytes, |
|
433 PAGE_EXECUTE_READ, &old_protection); |
|
434 |
|
435 ResultCode ret = child_->TransferVariable("g_originals", g_originals, |
|
436 sizeof(g_originals)); |
|
437 return (SBOX_ALL_OK == ret); |
|
438 } |
|
439 |
|
440 bool InterceptionManager::PatchClientFunctions(DllInterceptionData* thunks, |
|
441 size_t thunk_bytes, |
|
442 DllInterceptionData* dll_data) { |
|
443 DCHECK(NULL != thunks); |
|
444 DCHECK(NULL != dll_data); |
|
445 |
|
446 HMODULE ntdll_base = ::GetModuleHandle(kNtdllName); |
|
447 if (!ntdll_base) |
|
448 return false; |
|
449 |
|
450 base::win::PEImage ntdll_image(ntdll_base); |
|
451 |
|
452 // Bypass purify's interception. |
|
453 wchar_t* loader_get = reinterpret_cast<wchar_t*>( |
|
454 ntdll_image.GetProcAddress("LdrGetDllHandle")); |
|
455 if (loader_get) { |
|
456 if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | |
|
457 GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, |
|
458 loader_get, &ntdll_base)) |
|
459 return false; |
|
460 } |
|
461 |
|
462 if (base::win::GetVersion() <= base::win::VERSION_VISTA) { |
|
463 Wow64 WowHelper(child_, ntdll_base); |
|
464 if (!WowHelper.WaitForNtdll()) |
|
465 return false; |
|
466 } |
|
467 |
|
468 char* interceptor_base = NULL; |
|
469 |
|
470 #if SANDBOX_EXPORTS |
|
471 interceptor_base = reinterpret_cast<char*>(child_->MainModule()); |
|
472 HMODULE local_interceptor = ::LoadLibrary(child_->Name()); |
|
473 #endif |
|
474 |
|
475 ServiceResolverThunk* thunk; |
|
476 #if defined(_WIN64) |
|
477 thunk = new ServiceResolverThunk(child_->Process(), relaxed_); |
|
478 #else |
|
479 base::win::OSInfo* os_info = base::win::OSInfo::GetInstance(); |
|
480 if (os_info->wow64_status() == base::win::OSInfo::WOW64_ENABLED) { |
|
481 if (os_info->version() >= base::win::VERSION_WIN8) |
|
482 thunk = new Wow64W8ResolverThunk(child_->Process(), relaxed_); |
|
483 else |
|
484 thunk = new Wow64ResolverThunk(child_->Process(), relaxed_); |
|
485 } else if (!IsXPSP2OrLater()) { |
|
486 thunk = new Win2kResolverThunk(child_->Process(), relaxed_); |
|
487 } else if (os_info->version() >= base::win::VERSION_WIN8) { |
|
488 thunk = new Win8ResolverThunk(child_->Process(), relaxed_); |
|
489 } else { |
|
490 thunk = new ServiceResolverThunk(child_->Process(), relaxed_); |
|
491 } |
|
492 #endif |
|
493 |
|
494 std::list<InterceptionData>::iterator it = interceptions_.begin(); |
|
495 for (; it != interceptions_.end(); ++it) { |
|
496 const std::wstring ntdll(kNtdllName); |
|
497 if (it->dll != ntdll) |
|
498 break; |
|
499 |
|
500 if (INTERCEPTION_SERVICE_CALL != it->type) |
|
501 break; |
|
502 |
|
503 #if SANDBOX_EXPORTS |
|
504 // We may be trying to patch by function name. |
|
505 if (NULL == it->interceptor_address) { |
|
506 const char* address; |
|
507 NTSTATUS ret = thunk->ResolveInterceptor(local_interceptor, |
|
508 it->interceptor.c_str(), |
|
509 reinterpret_cast<const void**>( |
|
510 &address)); |
|
511 if (!NT_SUCCESS(ret)) |
|
512 break; |
|
513 |
|
514 // Translate the local address to an address on the child. |
|
515 it->interceptor_address = interceptor_base + (address - |
|
516 reinterpret_cast<char*>(local_interceptor)); |
|
517 } |
|
518 #endif |
|
519 NTSTATUS ret = thunk->Setup(ntdll_base, |
|
520 interceptor_base, |
|
521 it->function.c_str(), |
|
522 it->interceptor.c_str(), |
|
523 it->interceptor_address, |
|
524 &thunks->thunks[dll_data->num_thunks], |
|
525 thunk_bytes - dll_data->used_bytes, |
|
526 NULL); |
|
527 if (!NT_SUCCESS(ret)) |
|
528 break; |
|
529 |
|
530 DCHECK(!g_originals[it->id]); |
|
531 g_originals[it->id] = &thunks->thunks[dll_data->num_thunks]; |
|
532 |
|
533 dll_data->num_thunks++; |
|
534 dll_data->used_bytes += sizeof(ThunkData); |
|
535 } |
|
536 |
|
537 delete(thunk); |
|
538 |
|
539 #if SANDBOX_EXPORTS |
|
540 if (NULL != local_interceptor) |
|
541 ::FreeLibrary(local_interceptor); |
|
542 #endif |
|
543 |
|
544 if (it != interceptions_.end()) |
|
545 return false; |
|
546 |
|
547 return true; |
|
548 } |
|
549 |
|
550 } // namespace sandbox |