Merge pull request #4601 from MiniRML/fix/nid-pe-export-references
fix(nid): resolve PE reference exports through their ordinals
This commit is contained in:
commit
284cb63dc9
|
|
@ -198,6 +198,10 @@ if(BUILD_TESTING)
|
|||
endif()
|
||||
add_test_executable(patched_libraries_tests tests/PatchedLibraries.cpp)
|
||||
add_dependencies(patched_libraries_tests libs)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_pe_references COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_pe_references.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(MINGW)
|
||||
target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,12 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
|
|||
if (!directory.VirtualAddress || directory.Size < sizeof(PeExportDirectory)) throw std::runtime_error("missing reference PE exports");
|
||||
const auto sections = optional + optionalSize;
|
||||
RequireRange(binary, sections, std::uint64_t{sectionCount} * sizeof(PeSectionHeader));
|
||||
const auto headersSize = ReadValue<std::uint32_t>(binary, optional + 60);
|
||||
const auto mapRva = [&](std::uint32_t rva, std::uint64_t size) -> std::uint64_t {
|
||||
if (rva < headersSize && size <= headersSize - rva) {
|
||||
RequireRange(binary, rva, size);
|
||||
return rva;
|
||||
}
|
||||
for (std::uint16_t index = 0; index < sectionCount; ++index) {
|
||||
const auto section = ReadValue<PeSectionHeader>(binary, sections + std::uint64_t{index} * sizeof(PeSectionHeader));
|
||||
if (rva < section.VirtualAddress) continue;
|
||||
|
|
@ -65,14 +70,20 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
|
|||
const auto table = ReadValue<PeExportDirectory>(binary, mapRva(directory.VirtualAddress, sizeof(PeExportDirectory)));
|
||||
if (!table.NumberOfNames) throw std::runtime_error("reference PE has no named exports");
|
||||
const auto names = mapRva(table.AddressOfNames, std::uint64_t{table.NumberOfNames} * 4);
|
||||
const auto ordinals = mapRva(table.AddressOfNameOrdinals, std::uint64_t{table.NumberOfNames} * 2);
|
||||
const auto functions = mapRva(table.AddressOfFunctions, std::uint64_t{table.NumberOfFunctions} * 4);
|
||||
std::unordered_set<std::string> result;
|
||||
for (std::uint32_t index = 0; index < table.NumberOfNames; ++index) {
|
||||
const auto ordinal = ReadValue<std::uint16_t>(binary, ordinals + std::uint64_t{index} * 2);
|
||||
if (ordinal >= table.NumberOfFunctions) throw std::runtime_error("invalid reference PE export ordinal");
|
||||
if (!ReadValue<std::uint32_t>(binary, functions + std::uint64_t{ordinal} * 4)) continue;
|
||||
const auto rva = ReadValue<std::uint32_t>(binary, names + std::uint64_t{index} * 4);
|
||||
const auto offset = mapRva(rva, 1);
|
||||
auto name = ReadName(binary, offset, binary.size() - offset);
|
||||
mapRva(rva, name.size() + 1);
|
||||
if (!result.insert(name).second) throw std::runtime_error("duplicate reference export: " + name);
|
||||
}
|
||||
if (result.empty()) throw std::runtime_error("reference PE has no named exports");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_macho_nids import build_dylib, exports_of, nid
|
||||
|
||||
|
||||
def reference(*, headers=False, address=0x2000, ordinal=0):
|
||||
image = bytearray(1280)
|
||||
image[:2] = b"MZ"
|
||||
struct.pack_into("<I", image, 0x3c, 0x40)
|
||||
struct.pack_into("<IHHIIIHH", image, 0x40, 0x4550, 0x8664, 2, 0, 0, 0, 240, 0x2022)
|
||||
optional = 0x58
|
||||
struct.pack_into("<H", image, optional, 0x20b)
|
||||
struct.pack_into("<I", image, optional + 60, 512)
|
||||
struct.pack_into("<I", image, optional + 108, 16)
|
||||
start, rva = (416, 416) if headers else (512, 0x1000)
|
||||
struct.pack_into("<II", image, optional + 112, rva, 80)
|
||||
for index, (name, va, offset, size) in enumerate(((b".edata", 0x1000, 512, 256), (b".text", 0x2000, 768, 512))):
|
||||
struct.pack_into("<8sIIIIIIHHI", image, optional + 240 + index * 40,
|
||||
name, size, va, size, offset, 0, 0, 0, 0, 0x40000040)
|
||||
struct.pack_into("<IIHHIIIIIII", image, start, 0, 0, 0, 0, rva + 50, 1, 1, 1, rva + 40, rva + 44, rva + 48)
|
||||
struct.pack_into("<IIH", image, start + 40, address, rva + 50, ordinal)
|
||||
name = b"puts_nid_postfix\0"
|
||||
image[start + 50:start + 50 + len(name)] = name
|
||||
return image
|
||||
|
||||
|
||||
class ReferenceTests(unittest.TestCase):
|
||||
def patch_with_reference(self, data):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
source, ref = root / "target.dylib", root / "reference.bin"
|
||||
original = build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])
|
||||
source.write_bytes(original)
|
||||
ref.write_bytes(data)
|
||||
result = subprocess.run([str(PATCHER), "libc", str(source), "--preserve-exports", str(ref)], capture_output=True, text=True)
|
||||
self.assertEqual(ref.read_bytes(), data)
|
||||
return result, source.read_bytes(), original
|
||||
|
||||
def test_header_resident_export_tables(self):
|
||||
result, image, _ = self.patch_with_reference(reference(headers=True))
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(exports_of(image), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
|
||||
|
||||
def test_section_resident_export_tables(self):
|
||||
result, image, _ = self.patch_with_reference(reference())
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("_puts_nid_postfix", exports_of(image))
|
||||
|
||||
def test_null_function_slot_does_not_preserve_a_name(self):
|
||||
result, image, original = self.patch_with_reference(reference(address=0))
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("no named exports", result.stdout + result.stderr)
|
||||
self.assertEqual(image, original)
|
||||
|
||||
def test_invalid_name_ordinal_is_rejected_without_changing_target(self):
|
||||
result, image, original = self.patch_with_reference(reference(ordinal=1))
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("ordinal", result.stdout + result.stderr)
|
||||
self.assertEqual(image, original)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue