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 | |