fix(relinker): move a short guest TLS access's follower branch into its stub
A short fs-prefixed TLS access is patched with a 5-byte jump by relocating the following instructions into the stub, but any branch there was rejected outright. NHL 27 hits this on every stack-protector epilogue: fs:xor rax,[rbx] immediately followed by jne __stack_chk_fail. Allow relative conditional branches and short jumps: re-encode rel8 forms as their rel32 equivalents, rebase the displacement when the stub is emitted, and keep rejecting loop/jrcxz, calls, returns, and segment-prefixed followers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d146ec1b66
commit
c6f934a3d0
|
|
@ -345,7 +345,7 @@ if(BUILD_TESTING)
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $<TARGET_FILE:relinker>)
|
||||
set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads rejected)
|
||||
set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected)
|
||||
foreach(register IN ITEMS 0 1 2 3 5 6 7 8 9 10 11 12 13 14 15)
|
||||
list(APPEND tlsCoverageGroups displacement_r${register})
|
||||
endforeach()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ struct MovedInstruction {
|
|||
std::uint32_t Rva;
|
||||
std::vector<std::uint8_t> Bytes;
|
||||
std::optional<std::size_t> RipDisplacement;
|
||||
std::optional<std::uint32_t> BranchTarget;
|
||||
std::size_t BranchDisplacementOffset = 0;
|
||||
};
|
||||
|
||||
struct TlsAccess {
|
||||
|
|
@ -145,9 +147,28 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
|
|||
if (length >= header.FileSize - offset)
|
||||
throw Domain::RelinkerException("Short guest TLS instruction ends its segment", header.Offset + offset);
|
||||
const auto next = decoder.DecodeInstruction(bytes + length, header.FileSize - offset - length);
|
||||
if (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0)
|
||||
const bool relativeBranch =
|
||||
(next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) ||
|
||||
(next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb));
|
||||
if (!relativeBranch && (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0))
|
||||
throw Domain::RelinkerException("Short guest TLS instruction is followed by an instruction that cannot move", header.Offset + offset + length);
|
||||
moved.push_back({CheckedRva(rva + length), std::vector<std::uint8_t>(bytes + length, bytes + length + next.Length), next.HasRipRelativeDisp ? std::optional<std::size_t>(next.RipRelativeDispOffset) : std::nullopt});
|
||||
std::vector<std::uint8_t> movedBytes(bytes + length, bytes + length + next.Length);
|
||||
std::optional<std::uint32_t> branchTarget;
|
||||
std::size_t branchDisplacementOffset = 0;
|
||||
if (relativeBranch) {
|
||||
const auto target = static_cast<std::int64_t>(rva) + static_cast<std::int64_t>(length + next.Length) + next.BranchDisp;
|
||||
if (target < 0 || target > std::numeric_limits<std::uint32_t>::max())
|
||||
throw Domain::RelinkerException("Short guest TLS instruction is followed by a branch beyond the address space", header.Offset + offset + length);
|
||||
branchTarget = static_cast<std::uint32_t>(target);
|
||||
if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) {
|
||||
movedBytes = next.Opcode == 0xeb ? std::vector<std::uint8_t>{0xe9} : std::vector<std::uint8_t>{0x0f, static_cast<std::uint8_t>(0x80 | (next.Opcode & 0xf))};
|
||||
branchDisplacementOffset = movedBytes.size();
|
||||
movedBytes.insert(movedBytes.end(), 4, 0);
|
||||
} else {
|
||||
branchDisplacementOffset = next.Length - 4;
|
||||
}
|
||||
}
|
||||
moved.push_back({CheckedRva(rva + length), std::move(movedBytes), next.HasRipRelativeDisp ? std::optional<std::size_t>(next.RipRelativeDispOffset) : std::nullopt, branchTarget, branchDisplacementOffset});
|
||||
length += next.Length;
|
||||
}
|
||||
accesses.push_back({rva, header.Offset + offset, length, false, 0, addressRegister, 0, isAlu ? bytes[position] : std::uint8_t{0}, *address, wide, std::move(moved)});
|
||||
|
|
@ -357,6 +378,12 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
|
|||
throw Domain::RelinkerException("Moved RIP-relative instruction exceeds rel32 range from its TLS stub", access.FileOffset);
|
||||
Io::WriteU32(bytes, *instruction.RipDisplacement, static_cast<std::uint32_t>(static_cast<std::int32_t>(displacement)));
|
||||
}
|
||||
if (instruction.BranchTarget) {
|
||||
const auto displacement = static_cast<std::int64_t>(*instruction.BranchTarget) - (static_cast<std::int64_t>(code.GetRva()) + static_cast<std::int64_t>(bytes.size()));
|
||||
if (displacement < std::numeric_limits<std::int32_t>::min() || displacement > std::numeric_limits<std::int32_t>::max())
|
||||
throw Domain::RelinkerException("Moved branch exceeds rel32 range from its TLS stub", access.FileOffset);
|
||||
Io::WriteU32(bytes, instruction.BranchDisplacementOffset, static_cast<std::uint32_t>(static_cast<std::int32_t>(displacement)));
|
||||
}
|
||||
for (const auto byte : bytes)
|
||||
code.Emit({byte});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -511,7 +511,7 @@ def add_alias(image, size, begin=0x1200):
|
|||
|
||||
def main():
|
||||
groups = ("metadata", "padding", "register_loads", "displacement_bounds", "alu_execution",
|
||||
"register_address_loads", "rejected")
|
||||
"register_address_loads", "moved_branch", "rejected")
|
||||
groups += tuple(f"displacement_r{register}" for register in range(16) if register != 4)
|
||||
groups += tuple(f"alu_{name}" for name in ALU_READ)
|
||||
parser = argparse.ArgumentParser(description="Run Windows guest TLS function coverage fixtures")
|
||||
|
|
@ -699,12 +699,35 @@ def main():
|
|||
stub = pe_bytes_at(pe, 0x10000 + address + 5 + struct.unpack_from("<i", patched, 1)[0], 96)
|
||||
head, tail = register_alu_stub(instruction, opcode, register, wide)
|
||||
assert stub.startswith(head) and tail + moved in stub, (name, head.hex(), (tail + moved).hex(), stub.hex())
|
||||
if args.group in ("all", "moved_branch"):
|
||||
prologue = bytes.fromhex("48 c7 c3 f0 ff ff ff 48 31 c0")
|
||||
rel8_body = prologue + bytes.fromhex("64 48 33 03 75 06 31 c0 c3 90 90 90 b8 2a 00 00 00 c3")
|
||||
rel32_body = prologue + bytes.fromhex("64 48 33 03 0f 85") + struct.pack("<i", 0x1259 - 0x1254) + bytes.fromhex("31 c0 c3 90 90 b8 2a 00 00 00 c3")
|
||||
jmp_body = prologue + bytes.fromhex("64 48 33 03 eb 06 b8 01 00 00 00 c3 90 b8 2a 00 00 00 c3")
|
||||
for name, body, access, target, branch_opcode in (
|
||||
("moved-rel8-branch", rel8_body, 0x124A, 0x1256, "0f 85"),
|
||||
("moved-rel32-branch", rel32_body, 0x124A, 0x1259, "0f 85"),
|
||||
("moved-rel8-jump", jmp_body, 0x124A, 0x1256, "e9"),
|
||||
):
|
||||
image = make_image("register", "unwind", body=body)
|
||||
convert(name, image, tls_address=access)
|
||||
pe = (work / (name + ".exe")).read_bytes()
|
||||
patched = pe_bytes_at(pe, 0x10000 + access, 6)
|
||||
assert patched[0] == 0xe9 and patched[5] == 0x90, (name, patched.hex())
|
||||
stub_address = 0x10000 + access + 5 + struct.unpack_from("<i", patched, 1)[0]
|
||||
stub = pe_bytes_at(pe, stub_address, 128)
|
||||
encoding = bytes.fromhex(branch_opcode)
|
||||
offset = stub.find(encoding)
|
||||
assert offset >= 0, (name, stub.hex())
|
||||
displacement = struct.unpack_from("<i", stub, offset + len(encoding))[0]
|
||||
assert displacement == (0x10000 + target) - (stub_address + offset + len(encoding) + 4), (name, displacement)
|
||||
if args.group in ("all", "rejected"):
|
||||
for name, instruction in {
|
||||
"short-before-return": bytes.fromhex("64 48 8b 03 c3"),
|
||||
"short-before-call": bytes.fromhex("64 48 8b 03 e8 00 00 00 00 c3"),
|
||||
"short-before-tls": bytes.fromhex("64 48 8b 03 64 48 8b 03 c3"),
|
||||
"short-alu-before-return": bytes.fromhex("64 48 33 03 c3"),
|
||||
"short-before-loop": bytes.fromhex("64 48 33 03 e2 00"),
|
||||
}.items():
|
||||
convert(name, make_image("register", "unwind", body=instruction),
|
||||
"Short guest TLS instruction is followed by an instruction that cannot move", error_offset=0x1244)
|
||||
|
|
|
|||
Loading…
Reference in New Issue