| 1 | //===-- llvm-cfi-verify.cpp - CFI Verification tool for LLVM --------------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | // |
| 9 | // This tool verifies Control Flow Integrity (CFI) instrumentation by static |
| 10 | // binary analysis. See the design document in /docs/CFIVerify.rst for more |
| 11 | // information. |
| 12 | // |
| 13 | // This tool is currently incomplete. It currently only does disassembly for |
| 14 | // object files, and searches through the code for indirect control flow |
| 15 | // instructions, printing them once found. |
| 16 | // |
| 17 | //===----------------------------------------------------------------------===// |
| 18 | |
| 19 | #include "lib/FileAnalysis.h" |
| 20 | #include "lib/GraphBuilder.h" |
| 21 | |
| 22 | #include "llvm/BinaryFormat/ELF.h" |
| 23 | #include "llvm/DebugInfo/Symbolize/SymbolizableModule.h" |
| 24 | #include "llvm/Support/CommandLine.h" |
| 25 | #include "llvm/Support/Error.h" |
| 26 | #include "llvm/Support/FormatVariadic.h" |
| 27 | #include "llvm/Support/SpecialCaseList.h" |
| 28 | #include "llvm/Support/VirtualFileSystem.h" |
| 29 | |
| 30 | #include <cstdlib> |
| 31 | |
| 32 | using namespace llvm; |
| 33 | using namespace llvm::object; |
| 34 | using namespace llvm::cfi_verify; |
| 35 | |
| 36 | static cl::OptionCategory CFIVerifyCategory("CFI Verify Options" ); |
| 37 | |
| 38 | static cl::opt<std::string> InputFilename(cl::Positional, |
| 39 | cl::desc("<input file>" ), |
| 40 | cl::Required, |
| 41 | cl::cat(CFIVerifyCategory)); |
| 42 | static cl::opt<std::string> IgnorelistFilename(cl::Positional, |
| 43 | cl::desc("[ignorelist file]" ), |
| 44 | cl::init(Val: "-" ), |
| 45 | cl::cat(CFIVerifyCategory)); |
| 46 | static cl::opt<bool> PrintGraphs( |
| 47 | "print-graphs" , |
| 48 | cl::desc("Print graphs around indirect CF instructions in DOT format." ), |
| 49 | cl::init(Val: false), cl::cat(CFIVerifyCategory)); |
| 50 | static cl::opt<unsigned> PrintBlameContext( |
| 51 | "blame-context" , |
| 52 | cl::desc("Print the blame context (if possible) for BAD instructions. This " |
| 53 | "specifies the number of lines of context to include, where zero " |
| 54 | "disables this feature." ), |
| 55 | cl::init(Val: 0), cl::cat(CFIVerifyCategory)); |
| 56 | static cl::opt<unsigned> PrintBlameContextAll( |
| 57 | "blame-context-all" , |
| 58 | cl::desc("Prints the blame context (if possible) for ALL instructions. " |
| 59 | "This specifies the number of lines of context for non-BAD " |
| 60 | "instructions (see --blame-context). If --blame-context is " |
| 61 | "unspecified, it prints this number of contextual lines for BAD " |
| 62 | "instructions as well." ), |
| 63 | cl::init(Val: 0), cl::cat(CFIVerifyCategory)); |
| 64 | static cl::opt<bool> Summarize("summarize" , cl::desc("Print the summary only." ), |
| 65 | cl::init(Val: false), cl::cat(CFIVerifyCategory)); |
| 66 | |
| 67 | ExitOnError ExitOnErr; |
| 68 | |
| 69 | static void printBlameContext(const DILineInfo &LineInfo, unsigned Context) { |
| 70 | auto FileOrErr = MemoryBuffer::getFile(Filename: LineInfo.FileName); |
| 71 | if (!FileOrErr) { |
| 72 | errs() << "Could not open file: " << LineInfo.FileName << "\n" ; |
| 73 | return; |
| 74 | } |
| 75 | |
| 76 | std::unique_ptr<MemoryBuffer> File = std::move(FileOrErr.get()); |
| 77 | SmallVector<StringRef, 100> Lines; |
| 78 | File->getBuffer().split(A&: Lines, Separator: '\n'); |
| 79 | |
| 80 | for (unsigned i = std::max<size_t>(a: 1, b: LineInfo.Line - Context); |
| 81 | i < std::min<size_t>(a: Lines.size() + 1, b: LineInfo.Line + Context + 1); |
| 82 | ++i) { |
| 83 | if (i == LineInfo.Line) |
| 84 | outs() << ">" ; |
| 85 | else |
| 86 | outs() << " " ; |
| 87 | |
| 88 | outs() << i << ": " << Lines[i - 1] << "\n" ; |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | static void printInstructionInformation(const FileAnalysis &Analysis, |
| 93 | const Instr &InstrMeta, |
| 94 | const GraphResult &Graph, |
| 95 | CFIProtectionStatus ProtectionStatus) { |
| 96 | outs() << "Instruction: " << format_hex(N: InstrMeta.VMAddress, Width: 2) << " (" |
| 97 | << stringCFIProtectionStatus(Status: ProtectionStatus) << "): " ; |
| 98 | Analysis.printInstruction(InstrMeta, OS&: outs()); |
| 99 | outs() << " \n" ; |
| 100 | |
| 101 | if (PrintGraphs) |
| 102 | Graph.printToDOT(Analysis, OS&: outs()); |
| 103 | } |
| 104 | |
| 105 | static void printInstructionStatus(unsigned BlameLine, bool CFIProtected, |
| 106 | const DILineInfo &LineInfo) { |
| 107 | if (BlameLine) { |
| 108 | outs() << "Ignorelist Match: " << IgnorelistFilename << ":" << BlameLine |
| 109 | << "\n" ; |
| 110 | if (CFIProtected) |
| 111 | outs() << "====> Unexpected Protected\n" ; |
| 112 | else |
| 113 | outs() << "====> Expected Unprotected\n" ; |
| 114 | |
| 115 | if (PrintBlameContextAll) |
| 116 | printBlameContext(LineInfo, Context: PrintBlameContextAll); |
| 117 | } else { |
| 118 | if (CFIProtected) { |
| 119 | outs() << "====> Expected Protected\n" ; |
| 120 | if (PrintBlameContextAll) |
| 121 | printBlameContext(LineInfo, Context: PrintBlameContextAll); |
| 122 | } else { |
| 123 | outs() << "====> Unexpected Unprotected (BAD)\n" ; |
| 124 | if (PrintBlameContext) |
| 125 | printBlameContext(LineInfo, Context: PrintBlameContext); |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | static void |
| 131 | printIndirectCFInstructions(FileAnalysis &Analysis, |
| 132 | const SpecialCaseList *SpecialCaseList) { |
| 133 | uint64_t ExpectedProtected = 0; |
| 134 | uint64_t UnexpectedProtected = 0; |
| 135 | uint64_t ExpectedUnprotected = 0; |
| 136 | uint64_t UnexpectedUnprotected = 0; |
| 137 | |
| 138 | std::map<unsigned, uint64_t> BlameCounter; |
| 139 | |
| 140 | for (object::SectionedAddress Address : Analysis.getIndirectInstructions()) { |
| 141 | const auto &InstrMeta = Analysis.getInstructionOrDie(Address: Address.Address); |
| 142 | GraphResult Graph = GraphBuilder::buildFlowGraph(Analysis, Address); |
| 143 | |
| 144 | CFIProtectionStatus ProtectionStatus = |
| 145 | Analysis.validateCFIProtection(Graph); |
| 146 | bool CFIProtected = (ProtectionStatus == CFIProtectionStatus::PROTECTED); |
| 147 | |
| 148 | if (!Summarize) { |
| 149 | outs() << "-----------------------------------------------------\n" ; |
| 150 | printInstructionInformation(Analysis, InstrMeta, Graph, ProtectionStatus); |
| 151 | } |
| 152 | |
| 153 | if (IgnoreDWARFFlag) { |
| 154 | if (CFIProtected) |
| 155 | ExpectedProtected++; |
| 156 | else |
| 157 | UnexpectedUnprotected++; |
| 158 | continue; |
| 159 | } |
| 160 | |
| 161 | auto InliningInfo = Analysis.symbolizeInlinedCode(Address); |
| 162 | if (!InliningInfo || InliningInfo->getNumberOfFrames() == 0) { |
| 163 | errs() << "Failed to symbolise " << format_hex(N: Address.Address, Width: 2) |
| 164 | << " with line tables from " << InputFilename << "\n" ; |
| 165 | exit(EXIT_FAILURE); |
| 166 | } |
| 167 | |
| 168 | const auto &LineInfo = InliningInfo->getFrame(Index: 0); |
| 169 | |
| 170 | // Print the inlining symbolisation of this instruction. |
| 171 | if (!Summarize) { |
| 172 | for (uint32_t i = 0; i < InliningInfo->getNumberOfFrames(); ++i) { |
| 173 | const auto &Line = InliningInfo->getFrame(Index: i); |
| 174 | outs() << " " << format_hex(N: Address.Address, Width: 2) << " = " |
| 175 | << Line.FileName << ":" << Line.Line << ":" << Line.Column |
| 176 | << " (" << Line.FunctionName << ")\n" ; |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | if (!SpecialCaseList) { |
| 181 | if (CFIProtected) { |
| 182 | if (PrintBlameContextAll && !Summarize) |
| 183 | printBlameContext(LineInfo, Context: PrintBlameContextAll); |
| 184 | ExpectedProtected++; |
| 185 | } else { |
| 186 | if (PrintBlameContext && !Summarize) |
| 187 | printBlameContext(LineInfo, Context: PrintBlameContext); |
| 188 | UnexpectedUnprotected++; |
| 189 | } |
| 190 | continue; |
| 191 | } |
| 192 | |
| 193 | unsigned BlameLine = 0; |
| 194 | for (auto &K : {"cfi-icall" , "cfi-vcall" }) { |
| 195 | if (!BlameLine) { |
| 196 | auto [FileIdx, Line] = |
| 197 | SpecialCaseList->inSectionBlame(Section: K, Prefix: "src" , Query: LineInfo.FileName); |
| 198 | BlameLine = Line; |
| 199 | } |
| 200 | if (!BlameLine) { |
| 201 | auto [FileIdx, Line] = |
| 202 | SpecialCaseList->inSectionBlame(Section: K, Prefix: "fun" , Query: LineInfo.FunctionName); |
| 203 | BlameLine = Line; |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | if (BlameLine) { |
| 208 | BlameCounter[BlameLine]++; |
| 209 | if (CFIProtected) |
| 210 | UnexpectedProtected++; |
| 211 | else |
| 212 | ExpectedUnprotected++; |
| 213 | } else { |
| 214 | if (CFIProtected) |
| 215 | ExpectedProtected++; |
| 216 | else |
| 217 | UnexpectedUnprotected++; |
| 218 | } |
| 219 | |
| 220 | if (!Summarize) |
| 221 | printInstructionStatus(BlameLine, CFIProtected, LineInfo); |
| 222 | } |
| 223 | |
| 224 | uint64_t IndirectCFInstructions = ExpectedProtected + UnexpectedProtected + |
| 225 | ExpectedUnprotected + UnexpectedUnprotected; |
| 226 | |
| 227 | if (IndirectCFInstructions == 0) { |
| 228 | outs() << "No indirect CF instructions found.\n" ; |
| 229 | return; |
| 230 | } |
| 231 | |
| 232 | outs() << formatv(Fmt: "\nTotal Indirect CF Instructions: {0}\n" |
| 233 | "Expected Protected: {1} ({2:P})\n" |
| 234 | "Unexpected Protected: {3} ({4:P})\n" |
| 235 | "Expected Unprotected: {5} ({6:P})\n" |
| 236 | "Unexpected Unprotected (BAD): {7} ({8:P})\n" , |
| 237 | Vals&: IndirectCFInstructions, Vals&: ExpectedProtected, |
| 238 | Vals: ((double)ExpectedProtected) / IndirectCFInstructions, |
| 239 | Vals&: UnexpectedProtected, |
| 240 | Vals: ((double)UnexpectedProtected) / IndirectCFInstructions, |
| 241 | Vals&: ExpectedUnprotected, |
| 242 | Vals: ((double)ExpectedUnprotected) / IndirectCFInstructions, |
| 243 | Vals&: UnexpectedUnprotected, |
| 244 | Vals: ((double)UnexpectedUnprotected) / IndirectCFInstructions); |
| 245 | |
| 246 | if (!SpecialCaseList) |
| 247 | return; |
| 248 | |
| 249 | outs() << "\nIgnorelist Results:\n" ; |
| 250 | for (const auto &KV : BlameCounter) { |
| 251 | outs() << " " << IgnorelistFilename << ":" << KV.first << " affects " |
| 252 | << KV.second << " indirect CF instructions.\n" ; |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | int main(int argc, char **argv) { |
| 257 | cl::HideUnrelatedOptions(Categories: {&CFIVerifyCategory, &getColorCategory()}); |
| 258 | cl::ParseCommandLineOptions( |
| 259 | argc, argv, |
| 260 | Overview: "Identifies whether Control Flow Integrity protects all indirect control " |
| 261 | "flow instructions in the provided object file, DSO or binary.\nNote: " |
| 262 | "Anything statically linked into the provided file *must* be compiled " |
| 263 | "with '-g'. This can be relaxed through the '--ignore-dwarf' flag." ); |
| 264 | |
| 265 | InitializeAllTargetInfos(); |
| 266 | InitializeAllTargetMCs(); |
| 267 | InitializeAllAsmParsers(); |
| 268 | InitializeAllDisassemblers(); |
| 269 | |
| 270 | if (PrintBlameContextAll && !PrintBlameContext) |
| 271 | PrintBlameContext.setValue(V: PrintBlameContextAll); |
| 272 | |
| 273 | std::unique_ptr<SpecialCaseList> SpecialCaseList; |
| 274 | if (IgnorelistFilename != "-" ) { |
| 275 | std::string Error; |
| 276 | SpecialCaseList = SpecialCaseList::create(Paths: {IgnorelistFilename}, |
| 277 | FS&: *vfs::getRealFileSystem(), Error); |
| 278 | if (!SpecialCaseList) { |
| 279 | errs() << "Failed to get ignorelist: " << Error << "\n" ; |
| 280 | exit(EXIT_FAILURE); |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | FileAnalysis Analysis = ExitOnErr(FileAnalysis::Create(Filename: InputFilename)); |
| 285 | printIndirectCFInstructions(Analysis, SpecialCaseList: SpecialCaseList.get()); |
| 286 | |
| 287 | return EXIT_SUCCESS; |
| 288 | } |
| 289 | |