void HandleLongOrUnprintableString(std::wstring const& name, std::wstring const& description, std::size_t tabs, WarningType warning_type, std::string value) { std::wostream& out = GetOutputStreamW(); // TODO: Fix perf for extremely long names. Instead of reading indefinitely // and then checking the size after the fact, we should perform a bounded // read. auto const unprintable = FindFirstUnprintableClassicLocale(value); std::size_t const kMaxNameLength = 1024; if (unprintable != std::string::npos) { WriteNormal(out, L"WARNING! Detected unprintable " + description + L". Truncating.", tabs); WarnForCurrentFile(warning_type); value.erase(unprintable); } else if (value.size() > kMaxNameLength) { WriteNormal(out, L"WARNING! Detected suspiciously long " + description + L". Truncating.", tabs); WarnForCurrentFile(warning_type); value.erase(kMaxNameLength); } WriteNamedNormal(out, name, value.c_str(), tabs); }
void HandleLongOrUnprintableString(std::wstring const& name, std::wstring const& description, std::size_t tabs, WarningType warning_type, std::string value) { std::wostream& out = std::wcout; auto const unprintable = FindFirstUnprintableClassicLocale(value); std::size_t const kMaxNameLength = 1024; if (unprintable != std::string::npos) { WriteNormal(out, L"WARNING! Detected unprintable " + description + L". Truncating.", tabs); WarnForCurrentFile(warning_type); value.erase(unprintable); } else if (value.size() > kMaxNameLength) { WriteNormal(out, L"WARNING! Detected suspiciously long " + description + L". Truncating.", tabs); WarnForCurrentFile(warning_type); value.erase(kMaxNameLength); } WriteNamedNormal(out, name, value.c_str(), tabs); }
void DisassembleEp(hadesmem::Process const& process, hadesmem::PeFile const& pe_file, std::uintptr_t ep_rva, void* ep_va, std::size_t tabs) { if (!ep_va) { return; } std::wostream& out = GetOutputStreamW(); // Get the number of bytes from the EP to the end of the file. std::size_t max_buffer_size = GetBytesToEndOfFile(pe_file, ep_va); // Clamp the amount of data read to the theoretical maximum. std::size_t const kMaxInstructions = 10U; std::size_t const kMaxInstructionLen = 15U; std::size_t const kMaxInstructionsBytes = kMaxInstructions * kMaxInstructionLen; max_buffer_size = (std::min)(max_buffer_size, kMaxInstructionsBytes); auto const disasm_buf = hadesmem::ReadVector<std::uint8_t>(process, ep_va, max_buffer_size); std::uint64_t const ip = hadesmem::GetRuntimeBase(process, pe_file) + ep_rva; ud_t ud_obj; ud_init(&ud_obj); ud_set_input_buffer(&ud_obj, disasm_buf.data(), max_buffer_size); ud_set_syntax(&ud_obj, UD_SYN_INTEL); ud_set_pc(&ud_obj, ip); ud_set_mode(&ud_obj, pe_file.Is64() ? 64 : 32); // Be pessimistic. Use the minimum theoretical amount of instrutions we could // fit in our buffer. std::size_t const instruction_count = max_buffer_size / kMaxInstructionLen; for (std::size_t i = 0U; i < instruction_count; ++i) { std::uint32_t const len = ud_disassemble(&ud_obj); if (len == 0) { WriteNormal(out, L"WARNING! Disassembly failed.", tabs); // If we can't disassemble at least 5 instructions there's probably // something strange about the function. Even in the case of a nullsub // there is typically some INT3 or NOP padding after it... WarnForCurrentFile(i < 5U ? WarningType::kUnsupported : WarningType::kSuspicious); break; } char const* const asm_str = ud_insn_asm(&ud_obj); HADESMEM_DETAIL_ASSERT(asm_str); char const* const asm_bytes_str = ud_insn_hex(&ud_obj); HADESMEM_DETAIL_ASSERT(asm_bytes_str); auto const diasm_line = hadesmem::detail::MultiByteToWideChar(asm_str) + L" (" + hadesmem::detail::MultiByteToWideChar(asm_bytes_str) + L")"; WriteNormal(out, diasm_line, tabs); } }
void DumpSections(hadesmem::Process const& process, hadesmem::PeFile const& pe_file) { hadesmem::SectionList sections(process, pe_file); std::wostream& out = GetOutputStreamW(); if (std::begin(sections) != std::end(sections)) { WriteNewline(out); WriteNormal(out, L"Sections:", 1); } else { // Other checks on number of sections are done as part of header handling. hadesmem::NtHeaders const nt_hdrs(process, pe_file); if (nt_hdrs.GetNumberOfSections()) { WriteNewline(out); WriteNormal(out, L"WARNING! Section list is inavlid.", 1); WarnForCurrentFile(WarningType::kUnsupported); } } for (auto const& s : sections) { WriteNewline(out); if (s.IsVirtual()) { WriteNormal(out, L"WARNING! Section is virtual.", 2); WarnForCurrentFile(WarningType::kSuspicious); } HandleLongOrUnprintableString( L"name", L"section name", 2, WarningType::kSuspicious, s.GetName()); WriteNamedHex(out, L"VirtualAddress", s.GetVirtualAddress(), 2); WriteNamedHex(out, L"VirtualSize", s.GetVirtualSize(), 2); WriteNamedHex(out, L"PointerToRawData", s.GetPointerToRawData(), 2); WriteNamedHex(out, L"SizeOfRawData", s.GetSizeOfRawData(), 2); WriteNamedHex(out, L"PointerToRelocations", s.GetPointerToRelocations(), 2); WriteNamedHex(out, L"PointerToLinenumbers", s.GetPointerToLinenumbers(), 2); WriteNamedHex(out, L"NumberOfRelocations", s.GetNumberOfRelocations(), 2); WriteNamedHex(out, L"NumberOfLinenumbers", s.GetNumberOfLinenumbers(), 2); WriteNamedHex(out, L"Characteristics", s.GetCharacteristics(), 2); } }
void DumpTls(hadesmem::Process const& process, hadesmem::PeFile const& pe_file) { std::unique_ptr<hadesmem::TlsDir const> tls_dir; try { tls_dir = std::make_unique<hadesmem::TlsDir>(process, pe_file); } catch (std::exception const& /*e*/) { return; } std::wostream& out = GetOutputStreamW(); WriteNewline(out); WriteNormal(out, L"TLS:", 1); WriteNewline(out); WriteNamedHex( out, L"StartAddressOfRawData", tls_dir->GetStartAddressOfRawData(), 2); WriteNamedHex( out, L"EndAddressOfRawData", tls_dir->GetEndAddressOfRawData(), 2); WriteNamedHex(out, L"AddressOfIndex", tls_dir->GetAddressOfIndex(), 2); WriteNamedHex( out, L"AddressOfCallBacks", tls_dir->GetAddressOfCallBacks(), 2); if (tls_dir->GetAddressOfCallBacks()) { std::vector<ULONGLONG> callbacks; try { tls_dir->GetCallbacks(std::back_inserter(callbacks)); } catch (std::exception const& /*e*/) { WriteNormal(out, L"WARNING! TLS callbacks are inavlid.", 2); WarnForCurrentFile(WarningType::kSuspicious); } for (auto const& c : callbacks) { WriteNamedHex(out, L"Callback", static_cast<DWORD_PTR>(c), 2); } } WriteNamedHex(out, L"SizeOfZeroFill", tls_dir->GetSizeOfZeroFill(), 2); WriteNamedHex(out, L"Characteristics", tls_dir->GetCharacteristics(), 2); }
void DumpPeFile(hadesmem::Process const& process, hadesmem::PeFile const& pe_file, std::wstring const& path) { std::wostream& out = std::wcout; ClearWarnForCurrentFile(); std::uint32_t const k1MB = (1U << 20); std::uint32_t const k100MB = k1MB * 100; if (pe_file.GetSize() > k100MB) { // Not actually unsupported, just want to flag large files. WriteNewline(out); WriteNormal(out, L"WARNING! File is over 100MB.", 0); WarnForCurrentFile(WarningType::kUnsupported); } DumpHeaders(process, pe_file); DumpSections(process, pe_file); DumpTls(process, pe_file); DumpExports(process, pe_file); bool has_new_bound_imports_any = false; DumpImports(process, pe_file, has_new_bound_imports_any); DumpBoundImports(process, pe_file, has_new_bound_imports_any); DumpRelocations(process, pe_file); DumpStrings(process, pe_file); HandleWarnings(path); }
void DumpBoundImports(hadesmem::Process const& process, hadesmem::PeFile const& pe_file, bool has_new_bound_imports_any) { std::wostream& out = GetOutputStreamW(); if (!HasBoundImportDir(process, pe_file)) { // Sample: dllmaxvals.dll (Corkami PE Corpus) if (has_new_bound_imports_any) { WriteNewline(out); WriteNormal( out, L"WARNING! No bound import directory on file with an import dir " L"indicating the presence of a bound import dir.", 1); WarnForCurrentFile(WarningType::kSuspicious); } return; } if (!has_new_bound_imports_any) { WriteNewline(out); WriteNormal( out, L"WARNING! Seemingly valid bound import directory on file with an " L"import dir indicating no new bound import dir.", 1); WarnForCurrentFile(WarningType::kSuspicious); return; } hadesmem::BoundImportDescriptorList const bound_import_descs(process, pe_file); if (std::begin(bound_import_descs) == std::end(bound_import_descs)) { WriteNewline(out); WriteNormal(out, L"WARNING! Empty or invalid bound import directory.", 1); WarnForCurrentFile(WarningType::kSuspicious); return; } WriteNewline(out); WriteNormal(out, L"Bound Import Descriptors:", 1); std::uint32_t num_descs = 0U; for (auto const& desc : bound_import_descs) { WriteNewline(out); if (num_descs++ == 1000) { WriteNormal(out, L"WARNING! Processed 1000 bound import descriptors. Stopping " L"early to avoid resource exhaustion attacks.", 2); WarnForCurrentFile(WarningType::kUnsupported); break; } DWORD const time_date_stamp = desc.GetTimeDateStamp(); std::wstring time_date_stamp_str; if (!ConvertTimeStamp(time_date_stamp, time_date_stamp_str)) { WriteNormal(out, L"WARNING! Invalid timestamp.", 2); WarnForCurrentFile(WarningType::kSuspicious); } WriteNamedHexSuffix( out, L"TimeDateStamp", time_date_stamp, time_date_stamp_str, 2); WriteNamedHex(out, L"OffsetModuleName", desc.GetOffsetModuleName(), 2); WriteNamedNormal(out, L"ModuleName", desc.GetModuleName().c_str(), 2); WriteNamedHex(out, L"NumberOfModuleForwarderRefs", desc.GetNumberOfModuleForwarderRefs(), 2); hadesmem::BoundImportForwarderRefList const forwarder_refs( process, pe_file, desc); if (std::begin(forwarder_refs) != std::end(forwarder_refs)) { WriteNewline(out); WriteNormal(out, L"Module Forwarder Refs:", 2); } for (auto const& forwarder : forwarder_refs) { WriteNewline(out); DWORD const fwd_time_date_stamp = forwarder.GetTimeDateStamp(); std::wstring fwd_time_date_stamp_str; if (!ConvertTimeStamp(fwd_time_date_stamp, fwd_time_date_stamp_str)) { WriteNormal(out, L"WARNING! Invalid timestamp.", 3); WarnForCurrentFile(WarningType::kSuspicious); } WriteNamedHexSuffix( out, L"TimeDateStamp", fwd_time_date_stamp, fwd_time_date_stamp_str, 3); WriteNamedHex( out, L"OffsetModuleName", forwarder.GetOffsetModuleName(), 3); WriteNamedNormal( out, L"ModuleName", forwarder.GetModuleName().c_str(), 3); WriteNamedHex(out, L"Reserved", forwarder.GetReserved(), 3); } } }
void DumpExports(hadesmem::Process const& process, hadesmem::PeFile const& pe_file) { std::unique_ptr<hadesmem::ExportDir const> export_dir; try { export_dir = std::make_unique<hadesmem::ExportDir const>(process, pe_file); } catch (std::exception const& /*e*/) { return; } std::wostream& out = std::wcout; WriteNewline(out); WriteNormal(out, L"Export Dir:", 1); WriteNewline(out); WriteNamedHex(out, L"Characteristics", export_dir->GetCharacteristics(), 2); DWORD const time_date_stamp = export_dir->GetTimeDateStamp(); std::wstring time_date_stamp_str; if (!ConvertTimeStamp(time_date_stamp, time_date_stamp_str)) { WriteNormal(out, L"WARNING! Invalid timestamp.", 2); WarnForCurrentFile(WarningType::kSuspicious); } WriteNamedHexSuffix( out, L"TimeDateStamp", time_date_stamp, time_date_stamp_str, 2); WriteNamedHex(out, L"MajorVersion", export_dir->GetMajorVersion(), 2); WriteNamedHex(out, L"MinorVersion", export_dir->GetMinorVersion(), 2); WriteNamedHex(out, L"Name (Raw)", export_dir->GetNameRaw(), 2); // Name is not guaranteed to be valid. // Sample: dllord.dll (Corkami PE Corpus) try { auto name = export_dir->GetName(); HandleLongOrUnprintableString( L"Name", L"export module name", 2, WarningType::kSuspicious, name); } catch (std::exception const& /*e*/) { WriteNormal(out, L"WARNING! Failed to read export dir name.", 2); WarnForCurrentFile(WarningType::kSuspicious); } WriteNamedHex(out, L"OrdinalBase", export_dir->GetOrdinalBase(), 2); WriteNamedHex( out, L"NumberOfFunctions", export_dir->GetNumberOfFunctions(), 2); WriteNamedHex(out, L"NumberOfNames", export_dir->GetNumberOfNames(), 2); WriteNamedHex( out, L"AddressOfFunctions", export_dir->GetAddressOfFunctions(), 2); WriteNamedHex(out, L"AddressOfNames", export_dir->GetAddressOfNames(), 2); WriteNamedHex( out, L"AddressOfNameOrdinals", export_dir->GetAddressOfNameOrdinals(), 2); std::set<std::string> export_names; hadesmem::ExportList const exports(process, pe_file); if (std::begin(exports) != std::end(exports)) { WriteNewline(out); WriteNormal(out, L"Exports:", 2); } else { WriteNewline(out); WriteNormal(out, L"WARNING! Empty or invalid export list.", 2); WarnForCurrentFile(WarningType::kSuspicious); } std::uint32_t num_exports = 0U; for (auto const& e : exports) { WriteNewline(out); // Some legitimate DLLs have well over 1000 exports (e.g. ntdll.dll). if (num_exports++ == 10000) { WriteNormal( out, L"WARNING! Processed 10000 exports. Stopping early to avoid resource " L"exhaustion attacks.", 2); WarnForCurrentFile(WarningType::kSuspicious); break; } if (e.ByName()) { auto const name = e.GetName(); // Sample: dllweirdexp.dll HandleLongOrUnprintableString( L"Name", L"export name", 3, WarningType::kSuspicious, name); // PE files can have duplicate exported function names (or even have them // all identical) because the import hint is used to check the name first // before performing a search. // Sample: None ("Import name hint" section of "Undocumented PECOFF" // whitepaper). if (!export_names.insert(name).second) { WriteNormal(out, L"WARNING! Detected duplicate export name.", 3); WarnForCurrentFile(WarningType::kSuspicious); } } WriteNamedHex(out, L"ProcedureNumber", e.GetProcedureNumber(), 3); WriteNamedHex(out, L"OrdinalNumber", e.GetOrdinalNumber(), 3); if (e.IsForwarded()) { WriteNamedNormal(out, L"Forwarder", e.GetForwarder().c_str(), 3); WriteNamedNormal( out, L"ForwarderModule", e.GetForwarderModule().c_str(), 3); WriteNamedNormal( out, L"ForwarderFunction", e.GetForwarderFunction().c_str(), 3); WriteNamedNormal( out, L"IsForwardedByOrdinal", e.IsForwardedByOrdinal(), 3); if (e.IsForwardedByOrdinal()) { try { WriteNamedHex(out, L"ForwarderOrdinal", e.GetForwarderOrdinal(), 3); } catch (std::exception const& /*e*/) { WriteNormal(out, L"WARNING! ForwarderOrdinal invalid.", 3); WarnForCurrentFile(WarningType::kSuspicious); } } } else { auto const ep_rva = e.GetRva(); WriteNamedHex(out, L"RVA", e.GetRva(), 3); auto const ep_va = e.GetVa(); WriteNamedHex(out, L"VA", reinterpret_cast<std::uintptr_t>(ep_va), 3); if (ep_va) { DisassembleEp(process, pe_file, ep_rva, ep_va, 4); } else { WriteNormal(out, L"WARNING! Export VA is invalid.", 3); WarnForCurrentFile(WarningType::kSuspicious); } } } }
void DumpRelocations(hadesmem::Process const& process, hadesmem::PeFile const& pe_file) { if (!HasRelocationsDir(process, pe_file)) { return; } std::wostream& out = GetOutputStreamW(); WriteNewline(out); hadesmem::RelocationBlockList const reloc_blocks(process, pe_file); if (std::begin(reloc_blocks) != std::end(reloc_blocks)) { WriteNormal(out, L"Relocation Blocks:", 1); } else { // Sample: dllmaxvals.dll (Corkami PE Corpus) // Sample: fakerelocs.exe (Corkami PE Corpus) WriteNormal(out, L"WARNING! Relocation block list is invalid.", 1); WarnForCurrentFile(WarningType::kSuspicious); } for (auto const& block : reloc_blocks) { WriteNewline(out); auto const va = block.GetVirtualAddress(); WriteNamedHex(out, L"VirtualAddress", va, 2); auto const size = block.GetSizeOfBlock(); WriteNamedHex(out, L"SizeOfBlock", block.GetSizeOfBlock(), 2); WriteNewline(out); if (!size) { WriteNormal(out, L"WARNING! Detected zero sized relocation block.", 2); WarnForCurrentFile(WarningType::kUnsupported); continue; } WriteNormal(out, L"Relocations:", 2); hadesmem::RelocationList const relocs(process, pe_file, block.GetRelocationDataStart(), block.GetNumberOfRelocations()); for (auto const& reloc : relocs) { WriteNewline(out); auto const type = reloc.GetType(); WriteNamedHex(out, L"Type", type, 3); WriteNamedHex(out, L"Offset", reloc.GetOffset(), 3); // 11 = IMAGE_REL_BASED_HIGH3ADJ if (type > 11) { WriteNormal(out, L"WARNING! Unknown relocation type.", 3); WarnForCurrentFile(WarningType::kUnsupported); } } } }