fix(nid): validate GNU hash geometry before rewriting
This commit is contained in:
parent
2a5eca11cd
commit
bb443667ec
|
|
@ -163,6 +163,8 @@ if(BUILD_TESTING)
|
|||
endif()
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_gnu_hash COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_gnu_hash.py $<TARGET_FILE:nid_patcher>)
|
||||
add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -36,6 +36,18 @@ GnuHashLayout ReadGnuHashLayout(const std::vector<std::uint8_t>& elf, std::size_
|
|||
return layout;
|
||||
}
|
||||
|
||||
void ValidateGnuHash(const std::vector<std::uint8_t>& elf, std::size_t offset, std::size_t size, std::size_t symCount) {
|
||||
if (offset > elf.size() || size > elf.size() - offset || size < 16u)
|
||||
throw std::runtime_error("invalid gnu.hash section range");
|
||||
const auto layout = ReadGnuHashLayout(elf, offset);
|
||||
if (layout.SymOffset > symCount || !layout.BloomSize || (layout.BloomSize & (layout.BloomSize - 1u)) ||
|
||||
layout.BloomShift >= 32u || (!layout.NBuckets && layout.SymOffset != symCount))
|
||||
throw std::runtime_error("invalid gnu.hash geometry");
|
||||
const std::uint64_t required = 16u + std::uint64_t{layout.BloomSize} * 8u +
|
||||
std::uint64_t{layout.NBuckets} * 4u + (symCount - layout.SymOffset) * 4u;
|
||||
if (required > size) throw std::runtime_error("gnu.hash section too small to rebuild in place");
|
||||
}
|
||||
|
||||
void ReorderDynSymForGnuHash(
|
||||
std::vector<std::uint8_t>& elf,
|
||||
std::size_t dynSymOffset,
|
||||
|
|
@ -333,7 +345,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
if (shdr.sh_type == kShtGnuHash) {
|
||||
gnuHashOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
gnuHashSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
|
||||
}
|
||||
if (shdr.sh_type == kShtGnuVersym)
|
||||
versymOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
|
|
@ -345,6 +356,10 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
|
||||
const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym);
|
||||
if (symCount == 0u) throw std::runtime_error(".dynsym is empty");
|
||||
if (gnuHashOffset != 0u) {
|
||||
ValidateGnuHash(elf, gnuHashOffset, gnuHashSize, symCount);
|
||||
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
|
||||
}
|
||||
|
||||
const std::size_t dynStrSectionLink = [&]() -> std::size_t {
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash
|
||||
|
||||
|
||||
class GnuHashTests(unittest.TestCase):
|
||||
def image(self, buckets=4, bloom=1, shift=6, symoffset=3):
|
||||
image = build_image(symbols(False), False)
|
||||
count = len(symbols(False))
|
||||
struct.pack_into("<IIII", image, HASH_OFFSET, buckets, symoffset, bloom, shift)
|
||||
size = 16 + 8 * bloom + 4 * buckets + 4 * max(0, count - symoffset)
|
||||
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
|
||||
return image
|
||||
|
||||
def test_non_power_of_two_bloom_is_rejected(self):
|
||||
self.reject(self.image(bloom=3))
|
||||
|
||||
def test_hash_shift_outside_word_is_rejected(self):
|
||||
self.reject(self.image(shift=32))
|
||||
|
||||
def test_unsafe_geometry_is_rejected(self):
|
||||
for fields in ({"bloom": 0}, {"buckets": 0}, {"symoffset": 100}):
|
||||
with self.subTest(fields=fields):
|
||||
self.reject(self.image(**fields))
|
||||
|
||||
def test_declared_section_extent_is_checked(self):
|
||||
for size in (12, 16, 0x10000):
|
||||
with self.subTest(size=size):
|
||||
image = self.image()
|
||||
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
|
||||
self.reject(image)
|
||||
|
||||
def reject(self, image):
|
||||
result, after = patch(PATCHER, bytes(image), "geometry.so")
|
||||
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
|
||||
self.assertIn(b"gnu.hash", result.stderr)
|
||||
self.assertEqual(after, image)
|
||||
|
||||
def test_rebuilt_hash_finds_every_export(self):
|
||||
image = self.image(bloom=2)
|
||||
result, after = patch(PATCHER, image, "valid.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
entries = read_dynsym(after)
|
||||
buckets, first, bloom, shift = struct.unpack_from("<4I", after, HASH_OFFSET)
|
||||
bucket_offset = HASH_OFFSET + 16 + bloom * 8
|
||||
chain_offset = bucket_offset + buckets * 4
|
||||
for index in range(first, len(entries)):
|
||||
name = entries[index][0]
|
||||
hashed = gnu_hash(name)
|
||||
word = struct.unpack_from("<Q", after, HASH_OFFSET + 16 + ((hashed // 64) % bloom) * 8)[0]
|
||||
self.assertTrue(word & (1 << (hashed % 64)))
|
||||
self.assertTrue(word & (1 << ((hashed >> shift) % 64)))
|
||||
start = struct.unpack_from("<I", after, bucket_offset + (hashed % buckets) * 4)[0]
|
||||
found = False
|
||||
for candidate in range(start, len(entries)):
|
||||
value = struct.unpack_from("<I", after, chain_offset + (candidate - first) * 4)[0]
|
||||
if (value | 1) == (hashed | 1) and entries[candidate][0] == name:
|
||||
found = True
|
||||
if value & 1:
|
||||
break
|
||||
self.assertTrue(found, name)
|
||||
|
||||
def test_empty_hash_with_no_buckets_is_supported(self):
|
||||
image = self.image(buckets=0, symoffset=len(symbols(False)))
|
||||
result, after = patch(PATCHER, image, "empty.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(struct.unpack_from("<I", after, HASH_OFFSET)[0], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue