PE-sieve
Scans all running processes. Recognizes and dumps a variety of potentially malicious implants (replaced/implanted PEs, shellcodes, hooks, in-memory patches).
Loading...
Searching...
No Matches
code_scanner.cpp
Go to the documentation of this file.
1#include "code_scanner.h"
2
3#include <peconv.h>
4
5#include "patch_analyzer.h"
7//---
8#include <iostream>
9
10using namespace pesieve;
11using namespace pesieve::util;
12
13size_t pesieve::CodeScanReport::generateTags(const std::string& reportPath)
14{
15 if (patchesList.size() == 0) {
16 return 0;
17 }
18 std::ofstream patch_report;
19 patch_report.open(reportPath);
20 if (patch_report.is_open() == false) {
21 return 0;
22 }
23 size_t patches = patchesList.toTAGs(patch_report, ';');
24 if (patch_report.is_open()) {
25 patch_report.close();
26 }
27 return patches;
28}
29//---
30
31bool pesieve::CodeScanner::clearIAT(PeSection &originalSec, PeSection &remoteSec)
32{
33 // collect IAT fields:
34 std::set<DWORD> impThunkRVAs;
35 moduleData.loadImportThunks(impThunkRVAs);
36 if (impThunkRVAs.size() == 0) {
37 return false;
38 }
39
40 const size_t thunk_size = moduleData.is64bit() ? sizeof(ULONGLONG) : sizeof(DWORD);
41 std::set<DWORD>::iterator itr;
42 for (itr = impThunkRVAs.begin(); itr != impThunkRVAs.end(); ++itr) {
43 const DWORD iat_field = *itr;
44 // clear fields one by one:
45 if (originalSec.isContained(iat_field, thunk_size)) {
46 const DWORD offset = iat_field - originalSec.rva;
47 memset(originalSec.loadedSection + offset, 0, thunk_size);
48 memset(remoteSec.loadedSection + offset, 0, thunk_size);
49 }
50 }
51 return true;
52}
53
54bool pesieve::CodeScanner::clearLoadConfig(PeSection &originalSec, PeSection &remoteSec)
55{
56 // check if the Guard flag is enabled:
57 WORD charact = peconv::get_dll_characteristics(moduleData.original_module);
58 if ((charact & 0x4000) == 0) {
59 return false; //no guard flag
60 }
61 BYTE *ldconf_ptr = peconv::get_load_config_ptr(moduleData.original_module, moduleData.original_size);
62 if (!ldconf_ptr) return false;
63
64 peconv::t_load_config_ver ver = peconv::get_load_config_version(moduleData.original_module, moduleData.original_size, ldconf_ptr);
65 if (ver != peconv::LOAD_CONFIG_W8_VER && ver != peconv::LOAD_CONFIG_W10_VER) {
66 return false; // nothing to cleanup
67 }
68 ULONGLONG cflag_va = 0;
69 size_t field_size = 0;
70 if (this->moduleData.is64bit()) {
71 peconv::IMAGE_LOAD_CONFIG_DIR64_W8* ldc = (peconv::IMAGE_LOAD_CONFIG_DIR64_W8*) ldconf_ptr;
72 cflag_va = ldc->GuardCFCheckFunctionPointer;
73 field_size = sizeof(ULONGLONG);
74 }
75 else {
76 peconv::IMAGE_LOAD_CONFIG_DIR32_W8* ldc = (peconv::IMAGE_LOAD_CONFIG_DIR32_W8*) ldconf_ptr;
77 cflag_va = ldc->GuardCFCheckFunctionPointer;
78 field_size = sizeof(DWORD);
79 }
80 if (cflag_va == 0) return false;
81
82 const ULONGLONG module_base = (ULONG_PTR)moduleData.moduleHandle;
83 const ULONGLONG cflag_rva = cflag_va - module_base;
84 if (!originalSec.isContained(cflag_rva, field_size)) {
85 return false;
86 }
87 //clear the field:
88 size_t sec_offset = size_t(cflag_rva - originalSec.rva);
89 memset(originalSec.loadedSection + sec_offset, 0, field_size);
90 memset(remoteSec.loadedSection + sec_offset, 0, field_size);
91 return true;
92}
93
94bool pesieve::CodeScanner::clearExports(PeSection &originalSec, PeSection &remoteSec)
95{
96 IMAGE_DATA_DIRECTORY* dir = peconv::get_directory_entry(moduleData.original_module, IMAGE_DIRECTORY_ENTRY_EXPORT);
97 if (!dir) {
98 return false;
99 }
100 DWORD iat_rva = dir->VirtualAddress;
101 DWORD iat_size = dir->Size;
102
103 if (originalSec.isContained(iat_rva, iat_size))
104 {
105#ifdef _DEBUG
106 std::cout << "Exports are in the Code section!" << std::endl;
107#endif
108 DWORD offset = iat_rva - originalSec.rva;
109 IMAGE_EXPORT_DIRECTORY *exports = (IMAGE_EXPORT_DIRECTORY*) ((ULONGLONG)originalSec.loadedSection + offset);
110 if (!peconv::validate_ptr(originalSec.loadedSection, originalSec.loadedSize, exports, sizeof(IMAGE_EXPORT_DIRECTORY))) {
111 return false;
112 }
113 DWORD functions_offset = exports->AddressOfFunctions - originalSec.rva;
114 DWORD functions_count = exports->NumberOfFunctions;
115
116 const size_t func_area_size = functions_count * sizeof(DWORD);
117 if (!peconv::validate_ptr(originalSec.loadedSection, originalSec.loadedSize,
118 originalSec.loadedSection + functions_offset,
119 func_area_size))
120 {
121 return false;
122 }
123 memset(originalSec.loadedSection + functions_offset, 0, func_area_size);
124 memset(remoteSec.loadedSection + functions_offset, 0, func_area_size);
125 }
126 return true;
127}
128
129size_t pesieve::CodeScanner::collectPatches(DWORD section_rva, PBYTE orig_code, PBYTE patched_code, size_t code_size, OUT PatchList &patchesList)
130{
131 PatchAnalyzer analyzer(moduleData, section_rva, patched_code, code_size);
132 PatchList::Patch *currPatch = nullptr;
133
134 for (DWORD i = 0; i < (DWORD) code_size; i++) {
135 if (orig_code[i] == patched_code[i]) {
136 if (currPatch != nullptr) {
137 // close the patch
138 currPatch->setEnd(section_rva + i);
139 analyzer.analyzeOther(*currPatch);
140 currPatch = nullptr;
141 }
142 continue;
143 }
144 if (currPatch == nullptr) {
145 //open a new patch
146 currPatch = new(std::nothrow) PatchList::Patch(moduleData.moduleHandle, patchesList.size(), (DWORD) section_rva + i);
147 if (!currPatch) continue;
148 patchesList.insert(currPatch);
149 DWORD parsed_size = (DWORD) analyzer.analyzeHook(*currPatch);
150 if (parsed_size > 0) {
151 currPatch->setEnd(section_rva + i + parsed_size);
152 currPatch = nullptr; // close this patch
153 i += (parsed_size - 1); //subtract 1 because of i++ executed after continue
154 continue;
155 }
156 }
157 }
158 // if there is still unclosed patch, close it now:
159 if (currPatch != nullptr) {
160 //this happens if the patch lasts till the end of code, so, its end is the end of code
161 currPatch->setEnd(section_rva + (DWORD) code_size);
162 currPatch = nullptr;
163 }
164 return patchesList.size();
165}
166
167namespace pesieve {
168 inline BYTE* first_different(const BYTE *buf_ptr, size_t bif_size, const BYTE padding)
169 {
170 for (size_t i = 0; i < bif_size; i++) {
171 if (buf_ptr[i] != padding) {
172 return (BYTE*)(buf_ptr + i);
173 }
174 }
175 return nullptr;
176 }
177};
178
179CodeScanReport::t_section_status pesieve::CodeScanner::scanSection(PeSection &originalSec, PeSection &remoteSec, OUT PatchList &patchesList)
180{
181 if (!originalSec.isInitialized() || !remoteSec.isInitialized()) {
183 }
184 clearIAT(originalSec, remoteSec);
185 clearExports(originalSec, remoteSec);
186 clearLoadConfig(originalSec, remoteSec);
187 //TODO: handle sections that have inside Delayed Imports (they give false positives)
188
189 const size_t smaller_size = originalSec.loadedSize > remoteSec.loadedSize ? remoteSec.loadedSize : originalSec.loadedSize;
190#ifdef _DEBUG
191 std::cout << "Code RVA: "
192 << std::hex << originalSec.rva
193 << " to "
194 << std::hex << originalSec.loadedSize
195 << std::endl;
196#endif
197 //check if the code of the loaded module is same as the code of the module on the disk:
198 int res = memcmp(remoteSec.loadedSection, originalSec.loadedSection, smaller_size);
199
200 if ((originalSec.rawSize == 0 || peconv::is_padding(originalSec.loadedSection, smaller_size, 0))
201 && !peconv::is_padding(remoteSec.loadedSection, smaller_size, 0))
202 {
204 }
205
206 if (res != 0) {
207 collectPatches(originalSec.rva, originalSec.loadedSection, remoteSec.loadedSection, smaller_size, patchesList);
208 }
209
210 if (remoteSec.loadedSize > originalSec.loadedSize) {
211
212 const size_t diff = remoteSec.loadedSize - originalSec.loadedSize;
213 const BYTE *diff_bgn = remoteSec.loadedSection + originalSec.loadedSize;
214
215 BYTE *not_padding = first_different(diff_bgn, diff, 0);
216 if (not_padding) {
217 const DWORD found_offset = MASK_TO_DWORD((ULONG_PTR)not_padding - (ULONG_PTR)remoteSec.loadedSection);
218 const DWORD found_rva = remoteSec.rva + found_offset;
219 PatchList::Patch* currPatch = new(std::nothrow) PatchList::Patch(moduleData.moduleHandle, patchesList.size(), found_rva);
220 if (currPatch) {
221 currPatch->setEnd(MASK_TO_DWORD(remoteSec.rva + remoteSec.loadedSize));
222 patchesList.insert(currPatch);
223 }
224 }
225 }
226 if (patchesList.size()) {
228 }
229 if (res == 0) {
231 }
233}
234
235size_t pesieve::CodeScanner::collectExecutableSections(RemoteModuleData &_remoteModData, std::map<size_t, PeSection*> &sections, CodeScanReport &my_report)
236{
237 size_t initial_size = sections.size();
238 const size_t sec_count = peconv::get_sections_count(_remoteModData.headerBuffer, _remoteModData.getHeaderSize());
239 for (DWORD i = 0; i < sec_count; i++) {
240 PIMAGE_SECTION_HEADER section_hdr = peconv::get_section_hdr(_remoteModData.headerBuffer, _remoteModData.getHeaderSize(), i);
241 if (section_hdr == nullptr) {
242 continue;
243 }
244
245 const bool is_entry = _remoteModData.isSectionEntry(i);
246
247 if (!is_entry // entry section may be set as non executable, but it will still be executed
248 && !(section_hdr->Characteristics & IMAGE_SCN_MEM_EXECUTE)
249 && !_remoteModData.isSectionExecutable(i, this->isScanData, this->isScanInaccessible))
250 {
251 //not executable, skip it
252 continue;
253 }
254
255 //get the code section from the remote module:
256 PeSection *remoteSec = new(std::nothrow) PeSection(_remoteModData, i);
257 if (remoteSec && remoteSec->isInitialized()) {
258 if (is_entry // always scan section containing Entry Point
259 || is_code(remoteSec->loadedSection, remoteSec->loadedSize))
260 {
261 sections[i] = remoteSec;
262 continue;
263 }
264 }
265 else {
266 // report about failed initialization
268 }
269 // the section was not added to the list, delete it instead:
270 delete remoteSec;
271 }
272 //corner case: PEs without sections
273 if (sec_count == 0) {
274 PeSection *remoteSec = new(std::nothrow) PeSection(_remoteModData, 0);
275 if (remoteSec && remoteSec->isInitialized()) {
276 sections[0] = remoteSec;
277 }
278 else {
279 // report about failed initialization
281 // the section was not added to the list, delete it instead:
282 delete remoteSec;
283 }
284 }
285 return sections.size() - initial_size;
286}
287
288void pesieve::CodeScanner::freeExecutableSections(std::map<size_t, PeSection*> &sections)
289{
290 std::map<size_t, PeSection*>::iterator itr;
291 for (itr = sections.begin(); itr != sections.end(); ++itr) {
292 PeSection *sec = itr->second;
293 delete sec;
294 }
295 sections.clear();
296}
297
298t_scan_status pesieve::CodeScanner::scanUsingBase(
299 IN ULONGLONG load_base,
300 IN std::map<size_t, PeSection*> &remote_code,
301 OUT std::map<DWORD, CodeScanReport::t_section_status> &sectionToResult,
302 OUT PatchList &patchesList)
303{
305
306 // before scanning, ensure that the original module is relocated to the base where it was loaded
307 if (!moduleData.relocateToBase(load_base)) {
308 return SCAN_ERROR;
309 }
310
311 size_t errors = 0;
312 size_t modified = 0;
313 std::map<size_t, PeSection*>::iterator itr;
314
315 for (itr = remote_code.begin(); itr != remote_code.end(); ++itr) {
316 size_t sec_indx = itr->first;
317 PeSection *remoteSec = itr->second;
318
319 PeSection originalSec(moduleData, sec_indx);
320
321 CodeScanReport::t_section_status sec_status = scanSection(originalSec, *remoteSec, patchesList);
322 sectionToResult[originalSec.rva] = sec_status; //save the status for the section
323
324 if (sec_status == pesieve::CodeScanReport::SECTION_SCAN_ERR) errors++;
325 else if (sec_status != pesieve::CodeScanReport::SECTION_NOT_MODIFIED) {
326 modified++;
327 }
328 }
329
330 if (modified > 0) {
331 last_res = SCAN_SUSPICIOUS; //the highest priority for modified
332 }
333 else if (errors > 0) {
334 last_res = SCAN_ERROR;
335 }
336 return last_res;
337}
338
340{
341 if (!moduleData.isInitialized()) {
342 std::cerr << "[-] Module not initialized" << std::endl;
343 return nullptr;
344 }
345 if (!remoteModData.isInitialized()) {
346 std::cerr << "[-] Failed to read the module header" << std::endl;
347 return nullptr;
348 }
349 CodeScanReport *my_report = new(std::nothrow) CodeScanReport(moduleData.moduleHandle, remoteModData.getModuleSize());
350 if (!my_report) return nullptr; //this should not happen...
351
352 my_report->isDotNetModule = moduleData.isDotNet();
353
355 std::map<size_t, PeSection*> remote_code;
356
357 if (!collectExecutableSections(remoteModData, remote_code, *my_report)) {
358 my_report->status = last_res;
359 if (my_report->countInaccessibleSections() > 0) {
360 my_report->status = SCAN_ERROR;
361 }
362 return my_report;
363 }
364 const ULONGLONG load_base = (ULONGLONG)moduleData.moduleHandle;
365 const ULONGLONG hdr_base = remoteModData.getHdrImageBase();
366 my_report->origBase = moduleData.getHdrImageBase();
367 last_res = scanUsingBase(load_base, remote_code, my_report->sectionToResult, my_report->patchesList);
368
369 if (load_base != hdr_base && my_report->patchesList.size() > 0) {
370#ifdef _DEBUG
371 std::cout << "[WARNING] Load Base: " << std::hex << load_base << " is different than the Hdr Base: " << hdr_base << "\n";
372#endif
373 PatchList list2;
374 std::map<DWORD, CodeScanReport::t_section_status> section_to_result;
375 t_scan_status scan_res2 = scanUsingBase(hdr_base, remote_code, section_to_result, list2);
376 if (list2.size() < my_report->patchesList.size()) {
377 my_report->relocBase = hdr_base;
378 my_report->patchesList = list2;
379 my_report->sectionToResult = section_to_result;
380 last_res = scan_res2;
381 }
382#ifdef _DEBUG
383 std::cout << "Using patches list for the base: " << my_report->relocBase << " list size: " << my_report->patchesList.size() << "\n";
384#endif
385 }
386
387 this->freeExecutableSections(remote_code);
388 //post-process collected patches:
389 postProcessScan(*my_report);
390
391 my_report->status = last_res;
392 return my_report; // last result
393}
394
395bool pesieve::CodeScanner::postProcessScan(IN OUT CodeScanReport &report)
396{
397 // we need only exports from the current module, not the global mapping
398 if (report.patchesList.size() == 0) {
399 return false;
400 }
401 peconv::ExportsMapper local_mapper;
402 local_mapper.add_to_lookup(moduleData.szModName, (HMODULE) moduleData.original_module, (ULONGLONG) moduleData.moduleHandle);
403 report.patchesList.checkForHookedExports(local_mapper);
404 return true;
405}
A report from the code scan, generated by CodeScanner.
size_t generateTags(const std::string &reportPath)
enum pesieve::CodeScanReport::section_status t_section_status
size_t countInaccessibleSections()
std::map< DWORD, t_section_status > sectionToResult
virtual CodeScanReport * scanRemote()
A postprocessor of the detected code patches. Detects if the patch is a hook, and if so,...
void setEnd(DWORD end_rva)
Definition patch_list.h:52
const size_t toTAGs(std::ofstream &patch_report, const char delimiter)
Buffers the defined PE section belonging to the module loaded in the scanned process into the local m...
Definition pe_section.h:12
bool isContained(ULONGLONG field_start, size_t field_size)
Definition pe_section.h:36
Buffers the data from the module loaded in the scanned process into the local memory.
BYTE headerBuffer[peconv::MAX_HEADER_SIZE]
bool isSectionExecutable(const size_t section_number, bool allow_data, bool allow_inaccessible)
bool isSectionEntry(const size_t section_number)
#define MASK_TO_DWORD(val)
Definition iat_finder.h:9
bool is_code(BYTE *loadedData, size_t loadedSize)
DWORD(__stdcall *_PssCaptureSnapshot)(HANDLE ProcessHandle
BYTE * first_different(const BYTE *buf_ptr, size_t bif_size, const BYTE padding)
enum pesieve::module_scan_status t_scan_status
Final summary about the scanned process.